diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle index 5f60134227..823d43f00f 100644 --- a/apps/flutter_parent/android/app/build.gradle +++ b/apps/flutter_parent/android/app/build.gradle @@ -33,8 +33,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 31 - buildToolsVersion "30.0.2" + compileSdk 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -47,7 +46,7 @@ android { defaultConfig { applicationId "com.instructure.parentapp" minSdkVersion 26 - targetSdkVersion 31 + targetSdk 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -92,6 +91,7 @@ android { } } } + namespace 'com.instructure.parentapp' } flutter { @@ -99,7 +99,7 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20" testImplementation 'junit:junit:4.12' @@ -107,7 +107,7 @@ dependencies { androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation "com.squareup.okhttp3:okhttp:4.9.1" implementation 'org.jsoup:jsoup:1.11.3' - implementation 'com.google.gms:google-services:4.3.3' + implementation 'com.google.gms:google-services:4.3.14' } apply plugin: 'com.google.firebase.crashlytics' diff --git a/apps/flutter_parent/android/app/src/debug/AndroidManifest.xml b/apps/flutter_parent/android/app/src/debug/AndroidManifest.xml index 1e16c2e14f..f880684a6a 100644 --- a/apps/flutter_parent/android/app/src/debug/AndroidManifest.xml +++ b/apps/flutter_parent/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml index f4245b5921..1ed267dd39 100644 --- a/apps/flutter_parent/android/app/src/main/AndroidManifest.xml +++ b/apps/flutter_parent/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/apps/flutter_parent/android/build.gradle b/apps/flutter_parent/android/build.gradle index 2aad022d71..f73dbba37a 100644 --- a/apps/flutter_parent/android/build.gradle +++ b/apps/flutter_parent/android/build.gradle @@ -5,10 +5,10 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:4.1.0" - classpath "com.google.firebase:firebase-crashlytics-gradle:2.1.0" - classpath "com.google.gms:google-services:4.3.10" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30" + classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' + classpath "com.google.gms:google-services:4.3.14" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20" } } @@ -27,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/apps/flutter_parent/android/buildSrc/build.gradle.kts b/apps/flutter_parent/android/buildSrc/build.gradle.kts index dc6156325b..67b48fbd24 100644 --- a/apps/flutter_parent/android/buildSrc/build.gradle.kts +++ b/apps/flutter_parent/android/buildSrc/build.gradle.kts @@ -19,7 +19,7 @@ repositories { mavenCentral() } -val agpVersion = "4.1.0" +val agpVersion = "7.4.2" dependencies { implementation("com.android.tools.build:gradle:$agpVersion") diff --git a/apps/flutter_parent/android/buildSrc/src/main/groovy/TimingsListener.groovy b/apps/flutter_parent/android/buildSrc/src/main/groovy/TimingsListener.groovy index 180a842557..eea645a77d 100644 --- a/apps/flutter_parent/android/buildSrc/src/main/groovy/TimingsListener.groovy +++ b/apps/flutter_parent/android/buildSrc/src/main/groovy/TimingsListener.groovy @@ -145,9 +145,6 @@ class TimingsListener implements TaskExecutionListener, BuildListener { } - @Override - void buildStarted(Gradle gradle) {} - @Override void projectsEvaluated(Gradle gradle) {} diff --git a/apps/flutter_parent/android/gradle.properties b/apps/flutter_parent/android/gradle.properties index a6738207fd..41672498af 100644 --- a/apps/flutter_parent/android/gradle.properties +++ b/apps/flutter_parent/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true +android.enableR8=true \ No newline at end of file diff --git a/apps/flutter_parent/android/gradle/wrapper/gradle-wrapper.properties b/apps/flutter_parent/android/gradle/wrapper/gradle-wrapper.properties index 381f2b3ca5..3c472b99c6 100644 --- a/apps/flutter_parent/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/flutter_parent/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/apps/flutter_parent/flutter_parent_sdk_url b/apps/flutter_parent/flutter_parent_sdk_url new file mode 100644 index 0000000000..a717a27c2c --- /dev/null +++ b/apps/flutter_parent/flutter_parent_sdk_url @@ -0,0 +1 @@ +https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.13.2-stable.tar.xz \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index 4872b5fe70..0e831ca07e 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -12,6 +12,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/generated/messages_all.dart'; import 'package:intl/intl.dart'; @@ -78,8 +79,8 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate @override bool shouldReload(LocalizationsDelegate old) => false; - LocaleResolutionCallback resolution({Locale fallback, bool matchCountry = true}) { - return (Locale locale, Iterable supported) { + LocaleResolutionCallback resolution({Locale? fallback, bool matchCountry = true}) { + return (Locale? locale, Iterable supported) { return _resolve(locale, fallback, supported, matchCountry); }; } @@ -87,29 +88,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate /// /// Returns true if the specified locale is supported, false otherwise. /// - Locale _isSupported(Locale locale, bool shouldMatchCountry) { - if (locale == null) { - return null; - } - + Locale? _isSupported(Locale? locale, bool shouldMatchCountry) { // Must match language code and script code. // Must match country code if specified or will fall back to the generic language pack if we don't match country code. // i.e., match a passed in 'zh-Hans' to the supported 'zh' if [matchCountry] is false - return supportedLocales.firstWhere((Locale supportedLocale) { - final matchLanguage = (supportedLocale.languageCode == locale.languageCode); - final matchScript = (locale.scriptCode == supportedLocale.scriptCode); - final matchCountry = (supportedLocale.countryCode == locale.countryCode); + return supportedLocales.firstWhereOrNull((Locale supportedLocale) { + final matchLanguage = (supportedLocale.languageCode == locale?.languageCode); + final matchScript = (locale?.scriptCode == supportedLocale.scriptCode); + final matchCountry = (supportedLocale.countryCode == locale?.countryCode); final matchCountryFallback = - (true != shouldMatchCountry && (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty)); + (true != shouldMatchCountry && (supportedLocale.countryCode == null || supportedLocale.countryCode?.isEmpty == true)); return matchLanguage && matchScript && (matchCountry || matchCountryFallback); - }, orElse: () => null); + }); } /// /// Internal method to resolve a locale from a list of locales. /// - Locale _resolve(Locale locale, Locale fallback, Iterable supported, bool matchCountry) { + Locale? _resolve(Locale? locale, Locale? fallback, Iterable supported, bool matchCountry) { if (locale == Locale('zh', 'Hant')) { // Special case Traditional Chinese (server sends us zh-Hant but translators give us zh-HK) locale = Locale('zh', 'HK'); @@ -122,7 +119,7 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } } -AppLocalizations L10n(BuildContext context) => Localizations.of(context, AppLocalizations); +AppLocalizations L10n(BuildContext context) => Localizations.of(context, AppLocalizations) ?? AppLocalizations(); /// /// App Localization class. diff --git a/apps/flutter_parent/lib/l10n/generated/messages_all.dart b/apps/flutter_parent/lib/l10n/generated/messages_all.dart index 1268e6af9e..72c44c1749 100644 --- a/apps/flutter_parent/lib/l10n/generated/messages_all.dart +++ b/apps/flutter_parent/lib/l10n/generated/messages_all.dart @@ -51,7 +51,7 @@ import 'messages_sv_instk12.dart' as messages_sv_instk12; import 'messages_zh.dart' as messages_zh; import 'messages_zh_HK.dart' as messages_zh_hk; -typedef Future LibraryLoader(); +typedef Future? LibraryLoader(); Map _deferredLibraries = { 'ar': () => new Future.value(null), 'ca': () => new Future.value(null), @@ -90,7 +90,7 @@ Map _deferredLibraries = { 'zh_HK': () => new Future.value(null), }; -MessageLookupByLibrary _findExact(String localeName) { +MessageLookupByLibrary? _findExact(String localeName) { switch (localeName) { case 'ar': return messages_ar.messages; @@ -191,7 +191,7 @@ bool _messagesExistFor(String locale) { } } -MessageLookupByLibrary _findGeneratedMessagesFor(String locale) { +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); if (actualLocale == null) return null; diff --git a/apps/flutter_parent/lib/main.dart b/apps/flutter_parent/lib/main.dart index 102b50fc81..deae3fbbe4 100644 --- a/apps/flutter_parent/lib/main.dart +++ b/apps/flutter_parent/lib/main.dart @@ -17,11 +17,11 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; -import 'package:device_info/device_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/parent_app.dart'; @@ -34,21 +34,22 @@ import 'package:flutter_parent/utils/old_app_migration.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; void main() async { - WidgetsFlutterBinding.ensureInitialized(); + await WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); setupLocator(); runZonedGuarded>(() async { - await Future.wait([ - ApiPrefs.init(), - ThemePrefs.init(), - RemoteConfigUtils.initialize(), - CrashUtils.init(), - FlutterDownloader.initialize(), - DbUtil.init() - ]); + + await ApiPrefs.init(); + await ThemePrefs.init(); + await RemoteConfigUtils.initialize(); + await CrashUtils.init(); + await FlutterDownloader.initialize(); + await DbUtil.init(); + PandaRouter.init(); await FlutterDownloader.registerCallback(downloadCallback); @@ -59,13 +60,6 @@ void main() async { await locator().performMigrationIfNecessary(); // ApiPrefs must be initialized before calling this - if (Platform.isAndroid) { - final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo; - if (info.version.sdkInt >= 29) { - WebView.platform = SurfaceAndroidWebView(); - } - } - // Set environment properties for analytics. No need to await this. locator().setEnvironmentProperties(); @@ -74,8 +68,8 @@ void main() async { } @pragma('vm:entry-point') -void downloadCallback(String id, DownloadTaskStatus status, int progress) { - final SendPort send = +void downloadCallback(String id, int status, int progress) { + final SendPort? send = IsolateNameServer.lookupPortByName('downloader_send_port'); - send.send([id, status, progress]); + send?.send([id, status, progress]); } diff --git a/apps/flutter_parent/lib/models/account_creation_models/create_account_post_body.g.dart b/apps/flutter_parent/lib/models/account_creation_models/create_account_post_body.g.dart index 9e935155fb..d20d530d70 100644 --- a/apps/flutter_parent/lib/models/account_creation_models/create_account_post_body.g.dart +++ b/apps/flutter_parent/lib/models/account_creation_models/create_account_post_body.g.dart @@ -20,10 +20,10 @@ class _$CreateAccountPostBodySerializer final String wireName = 'CreateAccountPostBody'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateAccountPostBody object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'pseudonym', serializers.serialize(object.pseudonym, specifiedType: const FullType(PostPseudonym)), @@ -40,28 +40,28 @@ class _$CreateAccountPostBodySerializer @override CreateAccountPostBody deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateAccountPostBodyBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'pseudonym': result.pseudonym.replace(serializers.deserialize(value, - specifiedType: const FullType(PostPseudonym)) as PostPseudonym); + specifiedType: const FullType(PostPseudonym))! as PostPseudonym); break; case 'pairing_code': result.pairingCode.replace(serializers.deserialize(value, - specifiedType: const FullType(PostPairingCode)) + specifiedType: const FullType(PostPairingCode))! as PostPairingCode); break; case 'user': result.user.replace(serializers.deserialize(value, - specifiedType: const FullType(PostUser)) as PostUser); + specifiedType: const FullType(PostUser))! as PostUser); break; } } @@ -79,21 +79,18 @@ class _$CreateAccountPostBody extends CreateAccountPostBody { final PostUser user; factory _$CreateAccountPostBody( - [void Function(CreateAccountPostBodyBuilder) updates]) => - (new CreateAccountPostBodyBuilder()..update(updates)).build(); + [void Function(CreateAccountPostBodyBuilder)? updates]) => + (new CreateAccountPostBodyBuilder()..update(updates))._build(); - _$CreateAccountPostBody._({this.pseudonym, this.pairingCode, this.user}) + _$CreateAccountPostBody._( + {required this.pseudonym, required this.pairingCode, required this.user}) : super._() { - if (pseudonym == null) { - throw new BuiltValueNullFieldError('CreateAccountPostBody', 'pseudonym'); - } - if (pairingCode == null) { - throw new BuiltValueNullFieldError( - 'CreateAccountPostBody', 'pairingCode'); - } - if (user == null) { - throw new BuiltValueNullFieldError('CreateAccountPostBody', 'user'); - } + BuiltValueNullFieldError.checkNotNull( + pseudonym, r'CreateAccountPostBody', 'pseudonym'); + BuiltValueNullFieldError.checkNotNull( + pairingCode, r'CreateAccountPostBody', 'pairingCode'); + BuiltValueNullFieldError.checkNotNull( + user, r'CreateAccountPostBody', 'user'); } @override @@ -116,13 +113,17 @@ class _$CreateAccountPostBody extends CreateAccountPostBody { @override int get hashCode { - return $jf($jc( - $jc($jc(0, pseudonym.hashCode), pairingCode.hashCode), user.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, pseudonym.hashCode); + _$hash = $jc(_$hash, pairingCode.hashCode); + _$hash = $jc(_$hash, user.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateAccountPostBody') + return (newBuiltValueToStringHelper(r'CreateAccountPostBody') ..add('pseudonym', pseudonym) ..add('pairingCode', pairingCode) ..add('user', user)) @@ -132,31 +133,32 @@ class _$CreateAccountPostBody extends CreateAccountPostBody { class CreateAccountPostBodyBuilder implements Builder { - _$CreateAccountPostBody _$v; + _$CreateAccountPostBody? _$v; - PostPseudonymBuilder _pseudonym; + PostPseudonymBuilder? _pseudonym; PostPseudonymBuilder get pseudonym => _$this._pseudonym ??= new PostPseudonymBuilder(); - set pseudonym(PostPseudonymBuilder pseudonym) => + set pseudonym(PostPseudonymBuilder? pseudonym) => _$this._pseudonym = pseudonym; - PostPairingCodeBuilder _pairingCode; + PostPairingCodeBuilder? _pairingCode; PostPairingCodeBuilder get pairingCode => _$this._pairingCode ??= new PostPairingCodeBuilder(); - set pairingCode(PostPairingCodeBuilder pairingCode) => + set pairingCode(PostPairingCodeBuilder? pairingCode) => _$this._pairingCode = pairingCode; - PostUserBuilder _user; + PostUserBuilder? _user; PostUserBuilder get user => _$this._user ??= new PostUserBuilder(); - set user(PostUserBuilder user) => _$this._user = user; + set user(PostUserBuilder? user) => _$this._user = user; CreateAccountPostBodyBuilder(); CreateAccountPostBodyBuilder get _$this { - if (_$v != null) { - _pseudonym = _$v.pseudonym?.toBuilder(); - _pairingCode = _$v.pairingCode?.toBuilder(); - _user = _$v.user?.toBuilder(); + final $v = _$v; + if ($v != null) { + _pseudonym = $v.pseudonym.toBuilder(); + _pairingCode = $v.pairingCode.toBuilder(); + _user = $v.user.toBuilder(); _$v = null; } return this; @@ -164,19 +166,19 @@ class CreateAccountPostBodyBuilder @override void replace(CreateAccountPostBody other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateAccountPostBody; } @override - void update(void Function(CreateAccountPostBodyBuilder) updates) { + void update(void Function(CreateAccountPostBodyBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateAccountPostBody build() { + CreateAccountPostBody build() => _build(); + + _$CreateAccountPostBody _build() { _$CreateAccountPostBody _$result; try { _$result = _$v ?? @@ -185,7 +187,7 @@ class CreateAccountPostBodyBuilder pairingCode: pairingCode.build(), user: user.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'pseudonym'; pseudonym.build(); @@ -195,7 +197,7 @@ class CreateAccountPostBodyBuilder user.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateAccountPostBody', _$failedField, e.toString()); + r'CreateAccountPostBody', _$failedField, e.toString()); } rethrow; } @@ -204,4 +206,4 @@ class CreateAccountPostBodyBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/account_creation_models/post_pairing_code.g.dart b/apps/flutter_parent/lib/models/account_creation_models/post_pairing_code.g.dart index 979e0240a0..9aedcfa018 100644 --- a/apps/flutter_parent/lib/models/account_creation_models/post_pairing_code.g.dart +++ b/apps/flutter_parent/lib/models/account_creation_models/post_pairing_code.g.dart @@ -17,9 +17,9 @@ class _$PostPairingCodeSerializer final String wireName = 'PostPairingCode'; @override - Iterable serialize(Serializers serializers, PostPairingCode object, + Iterable serialize(Serializers serializers, PostPairingCode object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'code', serializers.serialize(object.code, specifiedType: const FullType(String)), ]; @@ -29,19 +29,19 @@ class _$PostPairingCodeSerializer @override PostPairingCode deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PostPairingCodeBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'code': result.code = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -54,13 +54,11 @@ class _$PostPairingCode extends PostPairingCode { @override final String code; - factory _$PostPairingCode([void Function(PostPairingCodeBuilder) updates]) => - (new PostPairingCodeBuilder()..update(updates)).build(); + factory _$PostPairingCode([void Function(PostPairingCodeBuilder)? updates]) => + (new PostPairingCodeBuilder()..update(updates))._build(); - _$PostPairingCode._({this.code}) : super._() { - if (code == null) { - throw new BuiltValueNullFieldError('PostPairingCode', 'code'); - } + _$PostPairingCode._({required this.code}) : super._() { + BuiltValueNullFieldError.checkNotNull(code, r'PostPairingCode', 'code'); } @override @@ -79,29 +77,33 @@ class _$PostPairingCode extends PostPairingCode { @override int get hashCode { - return $jf($jc(0, code.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, code.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('PostPairingCode')..add('code', code)) + return (newBuiltValueToStringHelper(r'PostPairingCode')..add('code', code)) .toString(); } } class PostPairingCodeBuilder implements Builder { - _$PostPairingCode _$v; + _$PostPairingCode? _$v; - String _code; - String get code => _$this._code; - set code(String code) => _$this._code = code; + String? _code; + String? get code => _$this._code; + set code(String? code) => _$this._code = code; PostPairingCodeBuilder(); PostPairingCodeBuilder get _$this { - if (_$v != null) { - _code = _$v.code; + final $v = _$v; + if ($v != null) { + _code = $v.code; _$v = null; } return this; @@ -109,23 +111,26 @@ class PostPairingCodeBuilder @override void replace(PostPairingCode other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PostPairingCode; } @override - void update(void Function(PostPairingCodeBuilder) updates) { + void update(void Function(PostPairingCodeBuilder)? updates) { if (updates != null) updates(this); } @override - _$PostPairingCode build() { - final _$result = _$v ?? new _$PostPairingCode._(code: code); + PostPairingCode build() => _build(); + + _$PostPairingCode _build() { + final _$result = _$v ?? + new _$PostPairingCode._( + code: BuiltValueNullFieldError.checkNotNull( + code, r'PostPairingCode', 'code')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/account_creation_models/post_pseudonym.g.dart b/apps/flutter_parent/lib/models/account_creation_models/post_pseudonym.g.dart index 5b0ab56055..a4602ac397 100644 --- a/apps/flutter_parent/lib/models/account_creation_models/post_pseudonym.g.dart +++ b/apps/flutter_parent/lib/models/account_creation_models/post_pseudonym.g.dart @@ -16,9 +16,9 @@ class _$PostPseudonymSerializer implements StructuredSerializer { final String wireName = 'PostPseudonym'; @override - Iterable serialize(Serializers serializers, PostPseudonym object, + Iterable serialize(Serializers serializers, PostPseudonym object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'unique_id', serializers.serialize(object.uniqueId, specifiedType: const FullType(String)), @@ -32,23 +32,23 @@ class _$PostPseudonymSerializer implements StructuredSerializer { @override PostPseudonym deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PostPseudonymBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'unique_id': result.uniqueId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'password': result.password = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -63,16 +63,15 @@ class _$PostPseudonym extends PostPseudonym { @override final String password; - factory _$PostPseudonym([void Function(PostPseudonymBuilder) updates]) => - (new PostPseudonymBuilder()..update(updates)).build(); + factory _$PostPseudonym([void Function(PostPseudonymBuilder)? updates]) => + (new PostPseudonymBuilder()..update(updates))._build(); - _$PostPseudonym._({this.uniqueId, this.password}) : super._() { - if (uniqueId == null) { - throw new BuiltValueNullFieldError('PostPseudonym', 'uniqueId'); - } - if (password == null) { - throw new BuiltValueNullFieldError('PostPseudonym', 'password'); - } + _$PostPseudonym._({required this.uniqueId, required this.password}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + uniqueId, r'PostPseudonym', 'uniqueId'); + BuiltValueNullFieldError.checkNotNull( + password, r'PostPseudonym', 'password'); } @override @@ -92,12 +91,16 @@ class _$PostPseudonym extends PostPseudonym { @override int get hashCode { - return $jf($jc($jc(0, uniqueId.hashCode), password.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, uniqueId.hashCode); + _$hash = $jc(_$hash, password.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('PostPseudonym') + return (newBuiltValueToStringHelper(r'PostPseudonym') ..add('uniqueId', uniqueId) ..add('password', password)) .toString(); @@ -106,22 +109,23 @@ class _$PostPseudonym extends PostPseudonym { class PostPseudonymBuilder implements Builder { - _$PostPseudonym _$v; + _$PostPseudonym? _$v; - String _uniqueId; - String get uniqueId => _$this._uniqueId; - set uniqueId(String uniqueId) => _$this._uniqueId = uniqueId; + String? _uniqueId; + String? get uniqueId => _$this._uniqueId; + set uniqueId(String? uniqueId) => _$this._uniqueId = uniqueId; - String _password; - String get password => _$this._password; - set password(String password) => _$this._password = password; + String? _password; + String? get password => _$this._password; + set password(String? password) => _$this._password = password; PostPseudonymBuilder(); PostPseudonymBuilder get _$this { - if (_$v != null) { - _uniqueId = _$v.uniqueId; - _password = _$v.password; + final $v = _$v; + if ($v != null) { + _uniqueId = $v.uniqueId; + _password = $v.password; _$v = null; } return this; @@ -129,24 +133,28 @@ class PostPseudonymBuilder @override void replace(PostPseudonym other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PostPseudonym; } @override - void update(void Function(PostPseudonymBuilder) updates) { + void update(void Function(PostPseudonymBuilder)? updates) { if (updates != null) updates(this); } @override - _$PostPseudonym build() { - final _$result = - _$v ?? new _$PostPseudonym._(uniqueId: uniqueId, password: password); + PostPseudonym build() => _build(); + + _$PostPseudonym _build() { + final _$result = _$v ?? + new _$PostPseudonym._( + uniqueId: BuiltValueNullFieldError.checkNotNull( + uniqueId, r'PostPseudonym', 'uniqueId'), + password: BuiltValueNullFieldError.checkNotNull( + password, r'PostPseudonym', 'password')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/account_creation_models/post_user.g.dart b/apps/flutter_parent/lib/models/account_creation_models/post_user.g.dart index f07c848964..dbad749267 100644 --- a/apps/flutter_parent/lib/models/account_creation_models/post_user.g.dart +++ b/apps/flutter_parent/lib/models/account_creation_models/post_user.g.dart @@ -15,9 +15,9 @@ class _$PostUserSerializer implements StructuredSerializer { final String wireName = 'PostUser'; @override - Iterable serialize(Serializers serializers, PostUser object, + Iterable serialize(Serializers serializers, PostUser object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), 'initial_enrollment_type', @@ -32,27 +32,27 @@ class _$PostUserSerializer implements StructuredSerializer { } @override - PostUser deserialize(Serializers serializers, Iterable serialized, + PostUser deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PostUserBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'initial_enrollment_type': result.initialEnrollmentType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'terms_of_use': result.termsOfUse = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -69,20 +69,19 @@ class _$PostUser extends PostUser { @override final bool termsOfUse; - factory _$PostUser([void Function(PostUserBuilder) updates]) => - (new PostUserBuilder()..update(updates)).build(); + factory _$PostUser([void Function(PostUserBuilder)? updates]) => + (new PostUserBuilder()..update(updates))._build(); - _$PostUser._({this.name, this.initialEnrollmentType, this.termsOfUse}) + _$PostUser._( + {required this.name, + required this.initialEnrollmentType, + required this.termsOfUse}) : super._() { - if (name == null) { - throw new BuiltValueNullFieldError('PostUser', 'name'); - } - if (initialEnrollmentType == null) { - throw new BuiltValueNullFieldError('PostUser', 'initialEnrollmentType'); - } - if (termsOfUse == null) { - throw new BuiltValueNullFieldError('PostUser', 'termsOfUse'); - } + BuiltValueNullFieldError.checkNotNull(name, r'PostUser', 'name'); + BuiltValueNullFieldError.checkNotNull( + initialEnrollmentType, r'PostUser', 'initialEnrollmentType'); + BuiltValueNullFieldError.checkNotNull( + termsOfUse, r'PostUser', 'termsOfUse'); } @override @@ -103,13 +102,17 @@ class _$PostUser extends PostUser { @override int get hashCode { - return $jf($jc($jc($jc(0, name.hashCode), initialEnrollmentType.hashCode), - termsOfUse.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, initialEnrollmentType.hashCode); + _$hash = $jc(_$hash, termsOfUse.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('PostUser') + return (newBuiltValueToStringHelper(r'PostUser') ..add('name', name) ..add('initialEnrollmentType', initialEnrollmentType) ..add('termsOfUse', termsOfUse)) @@ -118,28 +121,29 @@ class _$PostUser extends PostUser { } class PostUserBuilder implements Builder { - _$PostUser _$v; + _$PostUser? _$v; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _initialEnrollmentType; - String get initialEnrollmentType => _$this._initialEnrollmentType; - set initialEnrollmentType(String initialEnrollmentType) => + String? _initialEnrollmentType; + String? get initialEnrollmentType => _$this._initialEnrollmentType; + set initialEnrollmentType(String? initialEnrollmentType) => _$this._initialEnrollmentType = initialEnrollmentType; - bool _termsOfUse; - bool get termsOfUse => _$this._termsOfUse; - set termsOfUse(bool termsOfUse) => _$this._termsOfUse = termsOfUse; + bool? _termsOfUse; + bool? get termsOfUse => _$this._termsOfUse; + set termsOfUse(bool? termsOfUse) => _$this._termsOfUse = termsOfUse; PostUserBuilder(); PostUserBuilder get _$this { - if (_$v != null) { - _name = _$v.name; - _initialEnrollmentType = _$v.initialEnrollmentType; - _termsOfUse = _$v.termsOfUse; + final $v = _$v; + if ($v != null) { + _name = $v.name; + _initialEnrollmentType = $v.initialEnrollmentType; + _termsOfUse = $v.termsOfUse; _$v = null; } return this; @@ -147,27 +151,30 @@ class PostUserBuilder implements Builder { @override void replace(PostUser other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PostUser; } @override - void update(void Function(PostUserBuilder) updates) { + void update(void Function(PostUserBuilder)? updates) { if (updates != null) updates(this); } @override - _$PostUser build() { + PostUser build() => _build(); + + _$PostUser _build() { final _$result = _$v ?? new _$PostUser._( - name: name, - initialEnrollmentType: initialEnrollmentType, - termsOfUse: termsOfUse); + name: BuiltValueNullFieldError.checkNotNull( + name, r'PostUser', 'name'), + initialEnrollmentType: BuiltValueNullFieldError.checkNotNull( + initialEnrollmentType, r'PostUser', 'initialEnrollmentType'), + termsOfUse: BuiltValueNullFieldError.checkNotNull( + termsOfUse, r'PostUser', 'termsOfUse')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/account_notification.g.dart b/apps/flutter_parent/lib/models/account_notification.g.dart index cabbcb0653..52f81819c1 100644 --- a/apps/flutter_parent/lib/models/account_notification.g.dart +++ b/apps/flutter_parent/lib/models/account_notification.g.dart @@ -20,10 +20,10 @@ class _$AccountNotificationSerializer final String wireName = 'AccountNotification'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, AccountNotification object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'message', @@ -42,32 +42,31 @@ class _$AccountNotificationSerializer @override AccountNotification deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AccountNotificationBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'message': result.message = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'subject': result.subject = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'start_at': result.startAt = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -87,23 +86,22 @@ class _$AccountNotification extends AccountNotification { final String startAt; factory _$AccountNotification( - [void Function(AccountNotificationBuilder) updates]) => - (new AccountNotificationBuilder()..update(updates)).build(); - - _$AccountNotification._({this.id, this.message, this.subject, this.startAt}) + [void Function(AccountNotificationBuilder)? updates]) => + (new AccountNotificationBuilder()..update(updates))._build(); + + _$AccountNotification._( + {required this.id, + required this.message, + required this.subject, + required this.startAt}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('AccountNotification', 'id'); - } - if (message == null) { - throw new BuiltValueNullFieldError('AccountNotification', 'message'); - } - if (subject == null) { - throw new BuiltValueNullFieldError('AccountNotification', 'subject'); - } - if (startAt == null) { - throw new BuiltValueNullFieldError('AccountNotification', 'startAt'); - } + BuiltValueNullFieldError.checkNotNull(id, r'AccountNotification', 'id'); + BuiltValueNullFieldError.checkNotNull( + message, r'AccountNotification', 'message'); + BuiltValueNullFieldError.checkNotNull( + subject, r'AccountNotification', 'subject'); + BuiltValueNullFieldError.checkNotNull( + startAt, r'AccountNotification', 'startAt'); } @override @@ -127,14 +125,18 @@ class _$AccountNotification extends AccountNotification { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, id.hashCode), message.hashCode), subject.hashCode), - startAt.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, message.hashCode); + _$hash = $jc(_$hash, subject.hashCode); + _$hash = $jc(_$hash, startAt.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('AccountNotification') + return (newBuiltValueToStringHelper(r'AccountNotification') ..add('id', id) ..add('message', message) ..add('subject', subject) @@ -145,34 +147,35 @@ class _$AccountNotification extends AccountNotification { class AccountNotificationBuilder implements Builder { - _$AccountNotification _$v; + _$AccountNotification? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _message; - String get message => _$this._message; - set message(String message) => _$this._message = message; + String? _message; + String? get message => _$this._message; + set message(String? message) => _$this._message = message; - String _subject; - String get subject => _$this._subject; - set subject(String subject) => _$this._subject = subject; + String? _subject; + String? get subject => _$this._subject; + set subject(String? subject) => _$this._subject = subject; - String _startAt; - String get startAt => _$this._startAt; - set startAt(String startAt) => _$this._startAt = startAt; + String? _startAt; + String? get startAt => _$this._startAt; + set startAt(String? startAt) => _$this._startAt = startAt; AccountNotificationBuilder() { AccountNotification._initializeBuilder(this); } AccountNotificationBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _message = _$v.message; - _subject = _$v.subject; - _startAt = _$v.startAt; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _message = $v.message; + _subject = $v.subject; + _startAt = $v.startAt; _$v = null; } return this; @@ -180,25 +183,32 @@ class AccountNotificationBuilder @override void replace(AccountNotification other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$AccountNotification; } @override - void update(void Function(AccountNotificationBuilder) updates) { + void update(void Function(AccountNotificationBuilder)? updates) { if (updates != null) updates(this); } @override - _$AccountNotification build() { + AccountNotification build() => _build(); + + _$AccountNotification _build() { final _$result = _$v ?? new _$AccountNotification._( - id: id, message: message, subject: subject, startAt: startAt); + id: BuiltValueNullFieldError.checkNotNull( + id, r'AccountNotification', 'id'), + message: BuiltValueNullFieldError.checkNotNull( + message, r'AccountNotification', 'message'), + subject: BuiltValueNullFieldError.checkNotNull( + subject, r'AccountNotification', 'subject'), + startAt: BuiltValueNullFieldError.checkNotNull( + startAt, r'AccountNotification', 'startAt')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/account_permissions.g.dart b/apps/flutter_parent/lib/models/account_permissions.g.dart index 10c617cc49..3f6c9bc7b6 100644 --- a/apps/flutter_parent/lib/models/account_permissions.g.dart +++ b/apps/flutter_parent/lib/models/account_permissions.g.dart @@ -17,9 +17,10 @@ class _$AccountPermissionsSerializer final String wireName = 'AccountPermissions'; @override - Iterable serialize(Serializers serializers, AccountPermissions object, + Iterable serialize( + Serializers serializers, AccountPermissions object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'become_user', serializers.serialize(object.becomeUser, specifiedType: const FullType(bool)), @@ -30,20 +31,19 @@ class _$AccountPermissionsSerializer @override AccountPermissions deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AccountPermissionsBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'become_user': result.becomeUser = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -57,13 +57,12 @@ class _$AccountPermissions extends AccountPermissions { final bool becomeUser; factory _$AccountPermissions( - [void Function(AccountPermissionsBuilder) updates]) => - (new AccountPermissionsBuilder()..update(updates)).build(); + [void Function(AccountPermissionsBuilder)? updates]) => + (new AccountPermissionsBuilder()..update(updates))._build(); - _$AccountPermissions._({this.becomeUser}) : super._() { - if (becomeUser == null) { - throw new BuiltValueNullFieldError('AccountPermissions', 'becomeUser'); - } + _$AccountPermissions._({required this.becomeUser}) : super._() { + BuiltValueNullFieldError.checkNotNull( + becomeUser, r'AccountPermissions', 'becomeUser'); } @override @@ -83,12 +82,15 @@ class _$AccountPermissions extends AccountPermissions { @override int get hashCode { - return $jf($jc(0, becomeUser.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, becomeUser.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('AccountPermissions') + return (newBuiltValueToStringHelper(r'AccountPermissions') ..add('becomeUser', becomeUser)) .toString(); } @@ -96,19 +98,20 @@ class _$AccountPermissions extends AccountPermissions { class AccountPermissionsBuilder implements Builder { - _$AccountPermissions _$v; + _$AccountPermissions? _$v; - bool _becomeUser; - bool get becomeUser => _$this._becomeUser; - set becomeUser(bool becomeUser) => _$this._becomeUser = becomeUser; + bool? _becomeUser; + bool? get becomeUser => _$this._becomeUser; + set becomeUser(bool? becomeUser) => _$this._becomeUser = becomeUser; AccountPermissionsBuilder() { AccountPermissions._initializeBuilder(this); } AccountPermissionsBuilder get _$this { - if (_$v != null) { - _becomeUser = _$v.becomeUser; + final $v = _$v; + if ($v != null) { + _becomeUser = $v.becomeUser; _$v = null; } return this; @@ -116,23 +119,26 @@ class AccountPermissionsBuilder @override void replace(AccountPermissions other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$AccountPermissions; } @override - void update(void Function(AccountPermissionsBuilder) updates) { + void update(void Function(AccountPermissionsBuilder)? updates) { if (updates != null) updates(this); } @override - _$AccountPermissions build() { - final _$result = _$v ?? new _$AccountPermissions._(becomeUser: becomeUser); + AccountPermissions build() => _build(); + + _$AccountPermissions _build() { + final _$result = _$v ?? + new _$AccountPermissions._( + becomeUser: BuiltValueNullFieldError.checkNotNull( + becomeUser, r'AccountPermissions', 'becomeUser')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/alert.dart b/apps/flutter_parent/lib/models/alert.dart index 6f3d04ff2d..c68e6d57f3 100644 --- a/apps/flutter_parent/lib/models/alert.dart +++ b/apps/flutter_parent/lib/models/alert.dart @@ -46,7 +46,7 @@ abstract class Alert implements Built { AlertWorkflowState get workflowState; @BuiltValueField(wireName: 'action_date') - DateTime get actionDate; + DateTime? get actionDate; String get title; @@ -110,7 +110,7 @@ abstract class Alert implements Built { return htmlUrl.substring(index1, index2); } - String getCourseIdForGradeAlerts() { + String? getCourseIdForGradeAlerts() { if (alertType == AlertType.courseGradeLow || alertType == AlertType.courseGradeHigh) { return contextId; } else if (alertType == AlertType.assignmentGradeLow || alertType == AlertType.assignmentGradeHigh) { @@ -120,9 +120,9 @@ abstract class Alert implements Built { } } - String _getCourseIdFromUrl() { + String? _getCourseIdFromUrl() { RegExp regex = RegExp(r'/courses/(\d+)/'); - Match match = regex.firstMatch(htmlUrl); + Match? match = regex.firstMatch(htmlUrl); return (match != null && match.groupCount >= 1) ? match.group(1) : null; } } @@ -194,7 +194,7 @@ class AlertType extends EnumClass { case AlertType.courseGradeLow: return 'course_grade_low'; default: - return null; + return ''; } } } diff --git a/apps/flutter_parent/lib/models/alert.g.dart b/apps/flutter_parent/lib/models/alert.g.dart index 2c191cb66a..bfa3308e37 100644 --- a/apps/flutter_parent/lib/models/alert.g.dart +++ b/apps/flutter_parent/lib/models/alert.g.dart @@ -105,9 +105,9 @@ class _$AlertSerializer implements StructuredSerializer { final String wireName = 'Alert'; @override - Iterable serialize(Serializers serializers, Alert object, + Iterable serialize(Serializers serializers, Alert object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'observer_alert_threshold_id', @@ -125,9 +125,6 @@ class _$AlertSerializer implements StructuredSerializer { 'workflow_state', serializers.serialize(object.workflowState, specifiedType: const FullType(AlertWorkflowState)), - 'action_date', - serializers.serialize(object.actionDate, - specifiedType: const FullType(DateTime)), 'title', serializers.serialize(object.title, specifiedType: const FullType(String)), @@ -144,70 +141,76 @@ class _$AlertSerializer implements StructuredSerializer { serializers.serialize(object.lockedForUser, specifiedType: const FullType(bool)), ]; + Object? value; + value = object.actionDate; + + result + ..add('action_date') + ..add(serializers.serialize(value, + specifiedType: const FullType(DateTime))); return result; } @override - Alert deserialize(Serializers serializers, Iterable serialized, + Alert deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AlertBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'observer_alert_threshold_id': result.observerAlertThresholdId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'context_type': result.contextType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'context_id': result.contextId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'alert_type': result.alertType = serializers.deserialize(value, - specifiedType: const FullType(AlertType)) as AlertType; + specifiedType: const FullType(AlertType))! as AlertType; break; case 'workflow_state': result.workflowState = serializers.deserialize(value, - specifiedType: const FullType(AlertWorkflowState)) + specifiedType: const FullType(AlertWorkflowState))! as AlertWorkflowState; break; case 'action_date': result.actionDate = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'observer_id': result.observerId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'locked_for_user': result.lockedForUser = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -249,7 +252,8 @@ class _$AlertTypeSerializer implements PrimitiveSerializer { @override AlertType deserialize(Serializers serializers, Object serialized, {FullType specifiedType = FullType.unspecified}) => - AlertType.valueOf(_fromWire[serialized] ?? serialized as String); + AlertType.valueOf( + _fromWire[serialized] ?? (serialized is String ? serialized : '')); } class _$AlertWorkflowStateSerializer @@ -284,7 +288,7 @@ class _$Alert extends Alert { @override final AlertWorkflowState workflowState; @override - final DateTime actionDate; + final DateTime? actionDate; @override final String title; @override @@ -296,59 +300,37 @@ class _$Alert extends Alert { @override final bool lockedForUser; - factory _$Alert([void Function(AlertBuilder) updates]) => - (new AlertBuilder()..update(updates)).build(); + factory _$Alert([void Function(AlertBuilder)? updates]) => + (new AlertBuilder()..update(updates))._build(); _$Alert._( - {this.id, - this.observerAlertThresholdId, - this.contextType, - this.contextId, - this.alertType, - this.workflowState, + {required this.id, + required this.observerAlertThresholdId, + required this.contextType, + required this.contextId, + required this.alertType, + required this.workflowState, this.actionDate, - this.title, - this.userId, - this.observerId, - this.htmlUrl, - this.lockedForUser}) + required this.title, + required this.userId, + required this.observerId, + required this.htmlUrl, + required this.lockedForUser}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Alert', 'id'); - } - if (observerAlertThresholdId == null) { - throw new BuiltValueNullFieldError('Alert', 'observerAlertThresholdId'); - } - if (contextType == null) { - throw new BuiltValueNullFieldError('Alert', 'contextType'); - } - if (contextId == null) { - throw new BuiltValueNullFieldError('Alert', 'contextId'); - } - if (alertType == null) { - throw new BuiltValueNullFieldError('Alert', 'alertType'); - } - if (workflowState == null) { - throw new BuiltValueNullFieldError('Alert', 'workflowState'); - } - if (actionDate == null) { - throw new BuiltValueNullFieldError('Alert', 'actionDate'); - } - if (title == null) { - throw new BuiltValueNullFieldError('Alert', 'title'); - } - if (userId == null) { - throw new BuiltValueNullFieldError('Alert', 'userId'); - } - if (observerId == null) { - throw new BuiltValueNullFieldError('Alert', 'observerId'); - } - if (htmlUrl == null) { - throw new BuiltValueNullFieldError('Alert', 'htmlUrl'); - } - if (lockedForUser == null) { - throw new BuiltValueNullFieldError('Alert', 'lockedForUser'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Alert', 'id'); + BuiltValueNullFieldError.checkNotNull( + observerAlertThresholdId, r'Alert', 'observerAlertThresholdId'); + BuiltValueNullFieldError.checkNotNull(contextType, r'Alert', 'contextType'); + BuiltValueNullFieldError.checkNotNull(contextId, r'Alert', 'contextId'); + BuiltValueNullFieldError.checkNotNull(alertType, r'Alert', 'alertType'); + BuiltValueNullFieldError.checkNotNull( + workflowState, r'Alert', 'workflowState'); + BuiltValueNullFieldError.checkNotNull(title, r'Alert', 'title'); + BuiltValueNullFieldError.checkNotNull(userId, r'Alert', 'userId'); + BuiltValueNullFieldError.checkNotNull(observerId, r'Alert', 'observerId'); + BuiltValueNullFieldError.checkNotNull(htmlUrl, r'Alert', 'htmlUrl'); + BuiltValueNullFieldError.checkNotNull( + lockedForUser, r'Alert', 'lockedForUser'); } @override @@ -378,35 +360,26 @@ class _$Alert extends Alert { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc(0, id.hashCode), - observerAlertThresholdId - .hashCode), - contextType.hashCode), - contextId.hashCode), - alertType.hashCode), - workflowState.hashCode), - actionDate.hashCode), - title.hashCode), - userId.hashCode), - observerId.hashCode), - htmlUrl.hashCode), - lockedForUser.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, observerAlertThresholdId.hashCode); + _$hash = $jc(_$hash, contextType.hashCode); + _$hash = $jc(_$hash, contextId.hashCode); + _$hash = $jc(_$hash, alertType.hashCode); + _$hash = $jc(_$hash, workflowState.hashCode); + _$hash = $jc(_$hash, actionDate.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, observerId.hashCode); + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, lockedForUser.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Alert') + return (newBuiltValueToStringHelper(r'Alert') ..add('id', id) ..add('observerAlertThresholdId', observerAlertThresholdId) ..add('contextType', contextType) @@ -424,57 +397,57 @@ class _$Alert extends Alert { } class AlertBuilder implements Builder { - _$Alert _$v; + _$Alert? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _observerAlertThresholdId; - String get observerAlertThresholdId => _$this._observerAlertThresholdId; - set observerAlertThresholdId(String observerAlertThresholdId) => + String? _observerAlertThresholdId; + String? get observerAlertThresholdId => _$this._observerAlertThresholdId; + set observerAlertThresholdId(String? observerAlertThresholdId) => _$this._observerAlertThresholdId = observerAlertThresholdId; - String _contextType; - String get contextType => _$this._contextType; - set contextType(String contextType) => _$this._contextType = contextType; + String? _contextType; + String? get contextType => _$this._contextType; + set contextType(String? contextType) => _$this._contextType = contextType; - String _contextId; - String get contextId => _$this._contextId; - set contextId(String contextId) => _$this._contextId = contextId; + String? _contextId; + String? get contextId => _$this._contextId; + set contextId(String? contextId) => _$this._contextId = contextId; - AlertType _alertType; - AlertType get alertType => _$this._alertType; - set alertType(AlertType alertType) => _$this._alertType = alertType; + AlertType? _alertType; + AlertType? get alertType => _$this._alertType; + set alertType(AlertType? alertType) => _$this._alertType = alertType; - AlertWorkflowState _workflowState; - AlertWorkflowState get workflowState => _$this._workflowState; - set workflowState(AlertWorkflowState workflowState) => + AlertWorkflowState? _workflowState; + AlertWorkflowState? get workflowState => _$this._workflowState; + set workflowState(AlertWorkflowState? workflowState) => _$this._workflowState = workflowState; - DateTime _actionDate; - DateTime get actionDate => _$this._actionDate; - set actionDate(DateTime actionDate) => _$this._actionDate = actionDate; + DateTime? _actionDate; + DateTime? get actionDate => _$this._actionDate; + set actionDate(DateTime? actionDate) => _$this._actionDate = actionDate; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _observerId; - String get observerId => _$this._observerId; - set observerId(String observerId) => _$this._observerId = observerId; + String? _observerId; + String? get observerId => _$this._observerId; + set observerId(String? observerId) => _$this._observerId = observerId; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - bool _lockedForUser; - bool get lockedForUser => _$this._lockedForUser; - set lockedForUser(bool lockedForUser) => + bool? _lockedForUser; + bool? get lockedForUser => _$this._lockedForUser; + set lockedForUser(bool? lockedForUser) => _$this._lockedForUser = lockedForUser; AlertBuilder() { @@ -482,19 +455,20 @@ class AlertBuilder implements Builder { } AlertBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _observerAlertThresholdId = _$v.observerAlertThresholdId; - _contextType = _$v.contextType; - _contextId = _$v.contextId; - _alertType = _$v.alertType; - _workflowState = _$v.workflowState; - _actionDate = _$v.actionDate; - _title = _$v.title; - _userId = _$v.userId; - _observerId = _$v.observerId; - _htmlUrl = _$v.htmlUrl; - _lockedForUser = _$v.lockedForUser; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _observerAlertThresholdId = $v.observerAlertThresholdId; + _contextType = $v.contextType; + _contextId = $v.contextId; + _alertType = $v.alertType; + _workflowState = $v.workflowState; + _actionDate = $v.actionDate; + _title = $v.title; + _userId = $v.userId; + _observerId = $v.observerId; + _htmlUrl = $v.htmlUrl; + _lockedForUser = $v.lockedForUser; _$v = null; } return this; @@ -502,36 +476,46 @@ class AlertBuilder implements Builder { @override void replace(Alert other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Alert; } @override - void update(void Function(AlertBuilder) updates) { + void update(void Function(AlertBuilder)? updates) { if (updates != null) updates(this); } @override - _$Alert build() { + Alert build() => _build(); + + _$Alert _build() { final _$result = _$v ?? new _$Alert._( - id: id, - observerAlertThresholdId: observerAlertThresholdId, - contextType: contextType, - contextId: contextId, - alertType: alertType, - workflowState: workflowState, + id: BuiltValueNullFieldError.checkNotNull(id, r'Alert', 'id'), + observerAlertThresholdId: BuiltValueNullFieldError.checkNotNull( + observerAlertThresholdId, r'Alert', 'observerAlertThresholdId'), + contextType: BuiltValueNullFieldError.checkNotNull( + contextType, r'Alert', 'contextType'), + contextId: BuiltValueNullFieldError.checkNotNull( + contextId, r'Alert', 'contextId'), + alertType: BuiltValueNullFieldError.checkNotNull( + alertType, r'Alert', 'alertType'), + workflowState: BuiltValueNullFieldError.checkNotNull( + workflowState, r'Alert', 'workflowState'), actionDate: actionDate, - title: title, - userId: userId, - observerId: observerId, - htmlUrl: htmlUrl, - lockedForUser: lockedForUser); + title: + BuiltValueNullFieldError.checkNotNull(title, r'Alert', 'title'), + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'Alert', 'userId'), + observerId: BuiltValueNullFieldError.checkNotNull( + observerId, r'Alert', 'observerId'), + htmlUrl: BuiltValueNullFieldError.checkNotNull( + htmlUrl, r'Alert', 'htmlUrl'), + lockedForUser: + BuiltValueNullFieldError.checkNotNull(lockedForUser, r'Alert', 'lockedForUser')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/alert_threshold.dart b/apps/flutter_parent/lib/models/alert_threshold.dart index 4972c6c9bd..48819b674f 100644 --- a/apps/flutter_parent/lib/models/alert_threshold.dart +++ b/apps/flutter_parent/lib/models/alert_threshold.dart @@ -34,8 +34,7 @@ abstract class AlertThreshold implements Built serialize(Serializers serializers, AlertThreshold object, + Iterable serialize(Serializers serializers, AlertThreshold object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'alert_type', @@ -32,48 +32,48 @@ class _$AlertThresholdSerializer serializers.serialize(object.observerId, specifiedType: const FullType(String)), ]; - result.add('threshold'); - if (object.threshold == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.threshold, - specifiedType: const FullType(String))); - } + Object? value; + value = object.threshold; + + result + ..add('threshold') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override AlertThreshold deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AlertThresholdBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'alert_type': result.alertType = serializers.deserialize(value, - specifiedType: const FullType(AlertType)) as AlertType; + specifiedType: const FullType(AlertType))! as AlertType; break; case 'threshold': result.threshold = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'observer_id': result.observerId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -88,30 +88,28 @@ class _$AlertThreshold extends AlertThreshold { @override final AlertType alertType; @override - final String threshold; + final String? threshold; @override final String userId; @override final String observerId; - factory _$AlertThreshold([void Function(AlertThresholdBuilder) updates]) => - (new AlertThresholdBuilder()..update(updates)).build(); + factory _$AlertThreshold([void Function(AlertThresholdBuilder)? updates]) => + (new AlertThresholdBuilder()..update(updates))._build(); _$AlertThreshold._( - {this.id, this.alertType, this.threshold, this.userId, this.observerId}) + {required this.id, + required this.alertType, + this.threshold, + required this.userId, + required this.observerId}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('AlertThreshold', 'id'); - } - if (alertType == null) { - throw new BuiltValueNullFieldError('AlertThreshold', 'alertType'); - } - if (userId == null) { - throw new BuiltValueNullFieldError('AlertThreshold', 'userId'); - } - if (observerId == null) { - throw new BuiltValueNullFieldError('AlertThreshold', 'observerId'); - } + BuiltValueNullFieldError.checkNotNull(id, r'AlertThreshold', 'id'); + BuiltValueNullFieldError.checkNotNull( + alertType, r'AlertThreshold', 'alertType'); + BuiltValueNullFieldError.checkNotNull(userId, r'AlertThreshold', 'userId'); + BuiltValueNullFieldError.checkNotNull( + observerId, r'AlertThreshold', 'observerId'); } @override @@ -135,17 +133,19 @@ class _$AlertThreshold extends AlertThreshold { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc(0, id.hashCode), alertType.hashCode), - threshold.hashCode), - userId.hashCode), - observerId.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, alertType.hashCode); + _$hash = $jc(_$hash, threshold.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, observerId.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('AlertThreshold') + return (newBuiltValueToStringHelper(r'AlertThreshold') ..add('id', id) ..add('alertType', alertType) ..add('threshold', threshold) @@ -157,39 +157,40 @@ class _$AlertThreshold extends AlertThreshold { class AlertThresholdBuilder implements Builder { - _$AlertThreshold _$v; + _$AlertThreshold? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - AlertType _alertType; - AlertType get alertType => _$this._alertType; - set alertType(AlertType alertType) => _$this._alertType = alertType; + AlertType? _alertType; + AlertType? get alertType => _$this._alertType; + set alertType(AlertType? alertType) => _$this._alertType = alertType; - String _threshold; - String get threshold => _$this._threshold; - set threshold(String threshold) => _$this._threshold = threshold; + String? _threshold; + String? get threshold => _$this._threshold; + set threshold(String? threshold) => _$this._threshold = threshold; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _observerId; - String get observerId => _$this._observerId; - set observerId(String observerId) => _$this._observerId = observerId; + String? _observerId; + String? get observerId => _$this._observerId; + set observerId(String? observerId) => _$this._observerId = observerId; AlertThresholdBuilder() { AlertThreshold._initializeBuilder(this); } AlertThresholdBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _alertType = _$v.alertType; - _threshold = _$v.threshold; - _userId = _$v.userId; - _observerId = _$v.observerId; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _alertType = $v.alertType; + _threshold = $v.threshold; + _userId = $v.userId; + _observerId = $v.observerId; _$v = null; } return this; @@ -197,29 +198,33 @@ class AlertThresholdBuilder @override void replace(AlertThreshold other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$AlertThreshold; } @override - void update(void Function(AlertThresholdBuilder) updates) { + void update(void Function(AlertThresholdBuilder)? updates) { if (updates != null) updates(this); } @override - _$AlertThreshold build() { + AlertThreshold build() => _build(); + + _$AlertThreshold _build() { final _$result = _$v ?? new _$AlertThreshold._( - id: id, - alertType: alertType, + id: BuiltValueNullFieldError.checkNotNull( + id, r'AlertThreshold', 'id'), + alertType: BuiltValueNullFieldError.checkNotNull( + alertType, r'AlertThreshold', 'alertType'), threshold: threshold, - userId: userId, - observerId: observerId); + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'AlertThreshold', 'userId'), + observerId: BuiltValueNullFieldError.checkNotNull( + observerId, r'AlertThreshold', 'observerId')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/announcement.g.dart b/apps/flutter_parent/lib/models/announcement.g.dart index eea73e9d25..ed7235ba3a 100644 --- a/apps/flutter_parent/lib/models/announcement.g.dart +++ b/apps/flutter_parent/lib/models/announcement.g.dart @@ -16,9 +16,9 @@ class _$AnnouncementSerializer implements StructuredSerializer { final String wireName = 'Announcement'; @override - Iterable serialize(Serializers serializers, Announcement object, + Iterable serialize(Serializers serializers, Announcement object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'title', @@ -43,42 +43,42 @@ class _$AnnouncementSerializer implements StructuredSerializer { } @override - Announcement deserialize(Serializers serializers, Iterable serialized, + Announcement deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AnnouncementBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'message': result.message = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'posted_at': result.postedAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime))! as DateTime; break; case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'attachments': result.attachments.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(RemoteFile)])) - as BuiltList); + BuiltList, const [const FullType(RemoteFile)]))! + as BuiltList); break; } } @@ -101,35 +101,25 @@ class _$Announcement extends Announcement { @override final BuiltList attachments; - factory _$Announcement([void Function(AnnouncementBuilder) updates]) => - (new AnnouncementBuilder()..update(updates)).build(); + factory _$Announcement([void Function(AnnouncementBuilder)? updates]) => + (new AnnouncementBuilder()..update(updates))._build(); _$Announcement._( - {this.id, - this.title, - this.message, - this.postedAt, - this.htmlUrl, - this.attachments}) + {required this.id, + required this.title, + required this.message, + required this.postedAt, + required this.htmlUrl, + required this.attachments}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Announcement', 'id'); - } - if (title == null) { - throw new BuiltValueNullFieldError('Announcement', 'title'); - } - if (message == null) { - throw new BuiltValueNullFieldError('Announcement', 'message'); - } - if (postedAt == null) { - throw new BuiltValueNullFieldError('Announcement', 'postedAt'); - } - if (htmlUrl == null) { - throw new BuiltValueNullFieldError('Announcement', 'htmlUrl'); - } - if (attachments == null) { - throw new BuiltValueNullFieldError('Announcement', 'attachments'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Announcement', 'id'); + BuiltValueNullFieldError.checkNotNull(title, r'Announcement', 'title'); + BuiltValueNullFieldError.checkNotNull(message, r'Announcement', 'message'); + BuiltValueNullFieldError.checkNotNull( + postedAt, r'Announcement', 'postedAt'); + BuiltValueNullFieldError.checkNotNull(htmlUrl, r'Announcement', 'htmlUrl'); + BuiltValueNullFieldError.checkNotNull( + attachments, r'Announcement', 'attachments'); } @override @@ -153,17 +143,20 @@ class _$Announcement extends Announcement { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc($jc(0, id.hashCode), title.hashCode), message.hashCode), - postedAt.hashCode), - htmlUrl.hashCode), - attachments.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, message.hashCode); + _$hash = $jc(_$hash, postedAt.hashCode); + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, attachments.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Announcement') + return (newBuiltValueToStringHelper(r'Announcement') ..add('id', id) ..add('title', title) ..add('message', message) @@ -176,32 +169,32 @@ class _$Announcement extends Announcement { class AnnouncementBuilder implements Builder { - _$Announcement _$v; + _$Announcement? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - String _message; - String get message => _$this._message; - set message(String message) => _$this._message = message; + String? _message; + String? get message => _$this._message; + set message(String? message) => _$this._message = message; - DateTime _postedAt; - DateTime get postedAt => _$this._postedAt; - set postedAt(DateTime postedAt) => _$this._postedAt = postedAt; + DateTime? _postedAt; + DateTime? get postedAt => _$this._postedAt; + set postedAt(DateTime? postedAt) => _$this._postedAt = postedAt; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - ListBuilder _attachments; + ListBuilder? _attachments; ListBuilder get attachments => _$this._attachments ??= new ListBuilder(); - set attachments(ListBuilder attachments) => + set attachments(ListBuilder? attachments) => _$this._attachments = attachments; AnnouncementBuilder() { @@ -209,13 +202,14 @@ class AnnouncementBuilder } AnnouncementBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _title = _$v.title; - _message = _$v.message; - _postedAt = _$v.postedAt; - _htmlUrl = _$v.htmlUrl; - _attachments = _$v.attachments?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _title = $v.title; + _message = $v.message; + _postedAt = $v.postedAt; + _htmlUrl = $v.htmlUrl; + _attachments = $v.attachments.toBuilder(); _$v = null; } return this; @@ -223,37 +217,42 @@ class AnnouncementBuilder @override void replace(Announcement other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Announcement; } @override - void update(void Function(AnnouncementBuilder) updates) { + void update(void Function(AnnouncementBuilder)? updates) { if (updates != null) updates(this); } @override - _$Announcement build() { + Announcement build() => _build(); + + _$Announcement _build() { _$Announcement _$result; try { _$result = _$v ?? new _$Announcement._( - id: id, - title: title, - message: message, - postedAt: postedAt, - htmlUrl: htmlUrl, + id: BuiltValueNullFieldError.checkNotNull( + id, r'Announcement', 'id'), + title: BuiltValueNullFieldError.checkNotNull( + title, r'Announcement', 'title'), + message: BuiltValueNullFieldError.checkNotNull( + message, r'Announcement', 'message'), + postedAt: BuiltValueNullFieldError.checkNotNull( + postedAt, r'Announcement', 'postedAt'), + htmlUrl: BuiltValueNullFieldError.checkNotNull( + htmlUrl, r'Announcement', 'htmlUrl'), attachments: attachments.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'attachments'; attachments.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Announcement', _$failedField, e.toString()); + r'Announcement', _$failedField, e.toString()); } rethrow; } @@ -262,4 +261,4 @@ class AnnouncementBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/assignment.dart b/apps/flutter_parent/lib/models/assignment.dart index 30b7f756a7..6175b081eb 100644 --- a/apps/flutter_parent/lib/models/assignment.dart +++ b/apps/flutter_parent/lib/models/assignment.dart @@ -16,6 +16,7 @@ library assignment; import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_parent/models/lock_info.dart'; import 'package:flutter_parent/models/submission_wrapper.dart'; @@ -33,15 +34,12 @@ abstract class Assignment implements Built { String get id; - @nullable - String get name; + String? get name; - @nullable - String get description; + String? get description; - @nullable @BuiltValueField(wireName: 'due_at') - DateTime get dueAt; + DateTime? get dueAt; @BuiltValueField(wireName: 'points_possible') double get pointsPossible; @@ -49,34 +47,29 @@ abstract class Assignment implements Built { @BuiltValueField(wireName: 'course_id') String get courseId; - @nullable @BuiltValueField(wireName: 'grading_type') - GradingType get gradingType; + GradingType? get gradingType; - @nullable @BuiltValueField(wireName: 'html_url') - String get htmlUrl; + String? get htmlUrl; - @nullable - String get url; + String? get url; - @nullable @BuiltValueField(wireName: 'quiz_id') - String get quizId; // (Optional) id of the associated quiz (applies only when submission_types is ["online_quiz"]) + String? get quizId; // (Optional) id of the associated quiz (applies only when submission_types is ["online_quiz"]) @BuiltValueField(wireName: 'use_rubric_for_grading') bool get useRubricForGrading; /// Wrapper object to handle observer and non-observer submission case /// See SubmissionWrapper for more details - @nullable @BuiltValueField(wireName: 'submission') - SubmissionWrapper get submissionWrapper; + SubmissionWrapper? get submissionWrapper; /// This is used specifically for the observer -> assignment list case (all observee submissions are returned) /// If you are using the assignment/submission model for any other case use submissionWrapper.submission above. - Submission submission(String studentId) => - submissionWrapper?.submissionList?.firstWhere((submission) => submission.userId == studentId, orElse: () => null); + Submission? submission(String? studentId) => + submissionWrapper?.submissionList?.firstWhereOrNull((submission) => submission.userId == studentId); @BuiltValueField(wireName: 'assignment_group_id') String get assignmentGroupId; @@ -84,31 +77,27 @@ abstract class Assignment implements Built { int get position; @BuiltValueField(wireName: 'lock_info') - LockInfo get lockInfo; + LockInfo? get lockInfo; @BuiltValueField(wireName: 'locked_for_user') bool get lockedForUser; - @nullable @BuiltValueField(wireName: 'lock_at') - DateTime get lockAt; // Date the teacher no longer accepts submissions. + DateTime? get lockAt; // Date the teacher no longer accepts submissions. - @nullable @BuiltValueField(wireName: 'unlock_at') - DateTime get unlockAt; + DateTime? get unlockAt; - @nullable @BuiltValueField(wireName: 'lock_explanation') - String get lockExplanation; + String? get lockExplanation; @BuiltValueField(wireName: 'free_form_criterion_comments') bool get freeFormCriterionComments; bool get published; - @nullable @BuiltValueField(wireName: 'group_category_id') - String get groupCategoryId; + String? get groupCategoryId; @BuiltValueField(wireName: 'user_submitted') bool get userSubmitted; @@ -127,9 +116,8 @@ abstract class Assignment implements Built { bool get isStudioEnabled; - @nullable @BuiltValueField(wireName: 'submission_types') - BuiltList get submissionTypes; + BuiltList? get submissionTypes; static void _initializeBuilder(AssignmentBuilder b) => b ..pointsPossible = 0.0 @@ -146,16 +134,16 @@ abstract class Assignment implements Built { @BuiltValueField(serialize: false) bool get isFullyLocked { - if (lockInfo == null || lockInfo.isEmpty) return false; - if (lockInfo.hasModuleName) return true; - if (lockInfo.unlockAt != null && lockInfo.unlockAt.isAfter(DateTime.now())) return true; + if (lockInfo == null || lockInfo?.isEmpty == true) return false; + if (lockInfo!.hasModuleName) return true; + if (lockInfo!.unlockAt != null && lockInfo!.unlockAt!.isAfter(DateTime.now())) return true; return false; } bool isSubmittable() => submissionTypes?.every((type) => type == SubmissionTypes.onPaper || type == SubmissionTypes.none) != true; - SubmissionStatus getStatus({String studentId}) { + SubmissionStatus getStatus({required String? studentId}) { final submission = this.submission(studentId); if (!isSubmittable()) { return SubmissionStatus.NONE; @@ -171,7 +159,7 @@ abstract class Assignment implements Built { } // Returns true if the submission is marked as missing, or if it's pass due and either no submission or 'fake' submission - bool _isMissingSubmission(String studentId) { + bool _isMissingSubmission(String? studentId) { final submission = this.submission(studentId); if (submission?.missing == true) return true; @@ -182,9 +170,9 @@ abstract class Assignment implements Built { return isPastDue && (submission == null || (submission.attempt == 0 && submission.grade == null)); } - bool get isDiscussion => submissionTypes.contains(SubmissionTypes.discussionTopic); + bool get isDiscussion => submissionTypes?.contains(SubmissionTypes.discussionTopic) ?? false; - bool get isQuiz => submissionTypes.contains(SubmissionTypes.onlineQuiz); + bool get isQuiz => submissionTypes?.contains(SubmissionTypes.onlineQuiz) ?? false; bool isGradingTypeQuantitative() { return gradingType == GradingType.points || gradingType == GradingType.percent; diff --git a/apps/flutter_parent/lib/models/assignment.g.dart b/apps/flutter_parent/lib/models/assignment.g.dart index e04fe657b5..4b4ce37fd5 100644 --- a/apps/flutter_parent/lib/models/assignment.g.dart +++ b/apps/flutter_parent/lib/models/assignment.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of assignment; +part of 'assignment.dart'; // ************************************************************************** // BuiltValueGenerator @@ -114,9 +114,9 @@ class _$AssignmentSerializer implements StructuredSerializer { final String wireName = 'Assignment'; @override - Iterable serialize(Serializers serializers, Assignment object, + Iterable serialize(Serializers serializers, Assignment object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'points_possible', @@ -134,9 +134,6 @@ class _$AssignmentSerializer implements StructuredSerializer { 'position', serializers.serialize(object.position, specifiedType: const FullType(int)), - 'lock_info', - serializers.serialize(object.lockInfo, - specifiedType: const FullType(LockInfo)), 'locked_for_user', serializers.serialize(object.lockedForUser, specifiedType: const FullType(bool)), @@ -165,231 +162,225 @@ class _$AssignmentSerializer implements StructuredSerializer { serializers.serialize(object.isStudioEnabled, specifiedType: const FullType(bool)), ]; - result.add('name'); - if (object.name == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.name, - specifiedType: const FullType(String))); - } - result.add('description'); - if (object.description == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.description, - specifiedType: const FullType(String))); - } - result.add('due_at'); - if (object.dueAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.dueAt, + Object? value; + value = object.name; + + result + ..add('name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.description; + + result + ..add('description') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.dueAt; + + result + ..add('due_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('grading_type'); - if (object.gradingType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.gradingType, + value = object.gradingType; + + result + ..add('grading_type') + ..add(serializers.serialize(value, specifiedType: const FullType(GradingType))); - } - result.add('html_url'); - if (object.htmlUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.htmlUrl, - specifiedType: const FullType(String))); - } - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('quiz_id'); - if (object.quizId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.quizId, - specifiedType: const FullType(String))); - } - result.add('submission'); - if (object.submissionWrapper == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionWrapper, + value = object.htmlUrl; + + result + ..add('html_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.quizId; + + result + ..add('quiz_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.submissionWrapper; + + result + ..add('submission') + ..add(serializers.serialize(value, specifiedType: const FullType(SubmissionWrapper))); - } - result.add('lock_at'); - if (object.lockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockAt, + value = object.lockInfo; + + result + ..add('lock_info') + ..add(serializers.serialize(value, + specifiedType: const FullType(LockInfo))); + value = object.lockAt; + + result + ..add('lock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('unlock_at'); - if (object.unlockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.unlockAt, + value = object.unlockAt; + + result + ..add('unlock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('lock_explanation'); - if (object.lockExplanation == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockExplanation, - specifiedType: const FullType(String))); - } - result.add('group_category_id'); - if (object.groupCategoryId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.groupCategoryId, - specifiedType: const FullType(String))); - } - result.add('submission_types'); - if (object.submissionTypes == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionTypes, + value = object.lockExplanation; + + result + ..add('lock_explanation') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.groupCategoryId; + + result + ..add('group_category_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.submissionTypes; + + result + ..add('submission_types') + ..add(serializers.serialize(value, specifiedType: const FullType( BuiltList, const [const FullType(SubmissionTypes)]))); - } + return result; } @override - Assignment deserialize(Serializers serializers, Iterable serialized, + Assignment deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AssignmentBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'description': result.description = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'due_at': result.dueAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'points_possible': result.pointsPossible = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double))! as double; break; case 'course_id': result.courseId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'grading_type': result.gradingType = serializers.deserialize(value, - specifiedType: const FullType(GradingType)) as GradingType; + specifiedType: const FullType(GradingType)) as GradingType?; break; case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'quiz_id': result.quizId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'use_rubric_for_grading': result.useRubricForGrading = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'submission': result.submissionWrapper.replace(serializers.deserialize(value, - specifiedType: const FullType(SubmissionWrapper)) + specifiedType: const FullType(SubmissionWrapper))! as SubmissionWrapper); break; case 'assignment_group_id': result.assignmentGroupId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'position': result.position = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; case 'lock_info': result.lockInfo.replace(serializers.deserialize(value, - specifiedType: const FullType(LockInfo)) as LockInfo); + specifiedType: const FullType(LockInfo))! as LockInfo); break; case 'locked_for_user': result.lockedForUser = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'lock_at': result.lockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'unlock_at': result.unlockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'lock_explanation': result.lockExplanation = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'free_form_criterion_comments': result.freeFormCriterionComments = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'published': result.published = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'group_category_id': result.groupCategoryId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'user_submitted': result.userSubmitted = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'only_visible_to_overrides': result.onlyVisibleToOverrides = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'anonymous_peer_reviews': result.anonymousPeerReviews = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'moderated_grading': result.moderatedGrading = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'anonymous_grading': result.anonymousGrading = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'isStudioEnabled': result.isStudioEnabled = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'submission_types': result.submissionTypes.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(SubmissionTypes)])) - as BuiltList); + BuiltList, const [const FullType(SubmissionTypes)]))! + as BuiltList); break; } } @@ -429,7 +420,8 @@ class _$GradingTypeSerializer implements PrimitiveSerializer { @override GradingType deserialize(Serializers serializers, Object serialized, {FullType specifiedType = FullType.unspecified}) => - GradingType.valueOf(_fromWire[serialized] ?? serialized as String); + GradingType.valueOf( + _fromWire[serialized] ?? (serialized is String ? serialized : '')); } class _$SubmissionTypesSerializer @@ -468,54 +460,55 @@ class _$SubmissionTypesSerializer @override SubmissionTypes deserialize(Serializers serializers, Object serialized, {FullType specifiedType = FullType.unspecified}) => - SubmissionTypes.valueOf(_fromWire[serialized] ?? serialized as String); + SubmissionTypes.valueOf( + _fromWire[serialized] ?? (serialized is String ? serialized : '')); } class _$Assignment extends Assignment { @override final String id; @override - final String name; + final String? name; @override - final String description; + final String? description; @override - final DateTime dueAt; + final DateTime? dueAt; @override final double pointsPossible; @override final String courseId; @override - final GradingType gradingType; + final GradingType? gradingType; @override - final String htmlUrl; + final String? htmlUrl; @override - final String url; + final String? url; @override - final String quizId; + final String? quizId; @override final bool useRubricForGrading; @override - final SubmissionWrapper submissionWrapper; + final SubmissionWrapper? submissionWrapper; @override final String assignmentGroupId; @override final int position; @override - final LockInfo lockInfo; + final LockInfo? lockInfo; @override final bool lockedForUser; @override - final DateTime lockAt; + final DateTime? lockAt; @override - final DateTime unlockAt; + final DateTime? unlockAt; @override - final String lockExplanation; + final String? lockExplanation; @override final bool freeFormCriterionComments; @override final bool published; @override - final String groupCategoryId; + final String? groupCategoryId; @override final bool userSubmitted; @override @@ -529,92 +522,69 @@ class _$Assignment extends Assignment { @override final bool isStudioEnabled; @override - final BuiltList submissionTypes; + final BuiltList? submissionTypes; - factory _$Assignment([void Function(AssignmentBuilder) updates]) => - (new AssignmentBuilder()..update(updates)).build(); + factory _$Assignment([void Function(AssignmentBuilder)? updates]) => + (new AssignmentBuilder()..update(updates))._build(); _$Assignment._( - {this.id, + {required this.id, this.name, this.description, this.dueAt, - this.pointsPossible, - this.courseId, + required this.pointsPossible, + required this.courseId, this.gradingType, this.htmlUrl, this.url, this.quizId, - this.useRubricForGrading, + required this.useRubricForGrading, this.submissionWrapper, - this.assignmentGroupId, - this.position, + required this.assignmentGroupId, + required this.position, this.lockInfo, - this.lockedForUser, + required this.lockedForUser, this.lockAt, this.unlockAt, this.lockExplanation, - this.freeFormCriterionComments, - this.published, + required this.freeFormCriterionComments, + required this.published, this.groupCategoryId, - this.userSubmitted, - this.onlyVisibleToOverrides, - this.anonymousPeerReviews, - this.moderatedGrading, - this.anonymousGrading, - this.isStudioEnabled, + required this.userSubmitted, + required this.onlyVisibleToOverrides, + required this.anonymousPeerReviews, + required this.moderatedGrading, + required this.anonymousGrading, + required this.isStudioEnabled, this.submissionTypes}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Assignment', 'id'); - } - if (pointsPossible == null) { - throw new BuiltValueNullFieldError('Assignment', 'pointsPossible'); - } - if (courseId == null) { - throw new BuiltValueNullFieldError('Assignment', 'courseId'); - } - if (useRubricForGrading == null) { - throw new BuiltValueNullFieldError('Assignment', 'useRubricForGrading'); - } - if (assignmentGroupId == null) { - throw new BuiltValueNullFieldError('Assignment', 'assignmentGroupId'); - } - if (position == null) { - throw new BuiltValueNullFieldError('Assignment', 'position'); - } - if (lockInfo == null) { - throw new BuiltValueNullFieldError('Assignment', 'lockInfo'); - } - if (lockedForUser == null) { - throw new BuiltValueNullFieldError('Assignment', 'lockedForUser'); - } - if (freeFormCriterionComments == null) { - throw new BuiltValueNullFieldError( - 'Assignment', 'freeFormCriterionComments'); - } - if (published == null) { - throw new BuiltValueNullFieldError('Assignment', 'published'); - } - if (userSubmitted == null) { - throw new BuiltValueNullFieldError('Assignment', 'userSubmitted'); - } - if (onlyVisibleToOverrides == null) { - throw new BuiltValueNullFieldError( - 'Assignment', 'onlyVisibleToOverrides'); - } - if (anonymousPeerReviews == null) { - throw new BuiltValueNullFieldError('Assignment', 'anonymousPeerReviews'); - } - if (moderatedGrading == null) { - throw new BuiltValueNullFieldError('Assignment', 'moderatedGrading'); - } - if (anonymousGrading == null) { - throw new BuiltValueNullFieldError('Assignment', 'anonymousGrading'); - } - if (isStudioEnabled == null) { - throw new BuiltValueNullFieldError('Assignment', 'isStudioEnabled'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Assignment', 'id'); + BuiltValueNullFieldError.checkNotNull( + pointsPossible, r'Assignment', 'pointsPossible'); + BuiltValueNullFieldError.checkNotNull(courseId, r'Assignment', 'courseId'); + BuiltValueNullFieldError.checkNotNull( + useRubricForGrading, r'Assignment', 'useRubricForGrading'); + BuiltValueNullFieldError.checkNotNull( + assignmentGroupId, r'Assignment', 'assignmentGroupId'); + BuiltValueNullFieldError.checkNotNull(position, r'Assignment', 'position'); + BuiltValueNullFieldError.checkNotNull( + lockedForUser, r'Assignment', 'lockedForUser'); + BuiltValueNullFieldError.checkNotNull( + freeFormCriterionComments, r'Assignment', 'freeFormCriterionComments'); + BuiltValueNullFieldError.checkNotNull( + published, r'Assignment', 'published'); + BuiltValueNullFieldError.checkNotNull( + userSubmitted, r'Assignment', 'userSubmitted'); + BuiltValueNullFieldError.checkNotNull( + onlyVisibleToOverrides, r'Assignment', 'onlyVisibleToOverrides'); + BuiltValueNullFieldError.checkNotNull( + anonymousPeerReviews, r'Assignment', 'anonymousPeerReviews'); + BuiltValueNullFieldError.checkNotNull( + moderatedGrading, r'Assignment', 'moderatedGrading'); + BuiltValueNullFieldError.checkNotNull( + anonymousGrading, r'Assignment', 'anonymousGrading'); + BuiltValueNullFieldError.checkNotNull( + isStudioEnabled, r'Assignment', 'isStudioEnabled'); } @override @@ -661,49 +631,43 @@ class _$Assignment extends Assignment { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, id.hashCode), name.hashCode), description.hashCode), dueAt.hashCode), pointsPossible.hashCode), courseId.hashCode), gradingType.hashCode), htmlUrl.hashCode), url.hashCode), quizId.hashCode), - useRubricForGrading.hashCode), - submissionWrapper.hashCode), - assignmentGroupId.hashCode), - position.hashCode), - lockInfo.hashCode), - lockedForUser.hashCode), - lockAt.hashCode), - unlockAt.hashCode), - lockExplanation.hashCode), - freeFormCriterionComments.hashCode), - published.hashCode), - groupCategoryId.hashCode), - userSubmitted.hashCode), - onlyVisibleToOverrides.hashCode), - anonymousPeerReviews.hashCode), - moderatedGrading.hashCode), - anonymousGrading.hashCode), - isStudioEnabled.hashCode), - submissionTypes.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, dueAt.hashCode); + _$hash = $jc(_$hash, pointsPossible.hashCode); + _$hash = $jc(_$hash, courseId.hashCode); + _$hash = $jc(_$hash, gradingType.hashCode); + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, quizId.hashCode); + _$hash = $jc(_$hash, useRubricForGrading.hashCode); + _$hash = $jc(_$hash, submissionWrapper.hashCode); + _$hash = $jc(_$hash, assignmentGroupId.hashCode); + _$hash = $jc(_$hash, position.hashCode); + _$hash = $jc(_$hash, lockInfo.hashCode); + _$hash = $jc(_$hash, lockedForUser.hashCode); + _$hash = $jc(_$hash, lockAt.hashCode); + _$hash = $jc(_$hash, unlockAt.hashCode); + _$hash = $jc(_$hash, lockExplanation.hashCode); + _$hash = $jc(_$hash, freeFormCriterionComments.hashCode); + _$hash = $jc(_$hash, published.hashCode); + _$hash = $jc(_$hash, groupCategoryId.hashCode); + _$hash = $jc(_$hash, userSubmitted.hashCode); + _$hash = $jc(_$hash, onlyVisibleToOverrides.hashCode); + _$hash = $jc(_$hash, anonymousPeerReviews.hashCode); + _$hash = $jc(_$hash, moderatedGrading.hashCode); + _$hash = $jc(_$hash, anonymousGrading.hashCode); + _$hash = $jc(_$hash, isStudioEnabled.hashCode); + _$hash = $jc(_$hash, submissionTypes.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Assignment') + return (newBuiltValueToStringHelper(r'Assignment') ..add('id', id) ..add('name', name) ..add('description', description) @@ -738,139 +702,140 @@ class _$Assignment extends Assignment { } class AssignmentBuilder implements Builder { - _$Assignment _$v; + _$Assignment? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _description; - String get description => _$this._description; - set description(String description) => _$this._description = description; + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; - DateTime _dueAt; - DateTime get dueAt => _$this._dueAt; - set dueAt(DateTime dueAt) => _$this._dueAt = dueAt; + DateTime? _dueAt; + DateTime? get dueAt => _$this._dueAt; + set dueAt(DateTime? dueAt) => _$this._dueAt = dueAt; - double _pointsPossible; - double get pointsPossible => _$this._pointsPossible; - set pointsPossible(double pointsPossible) => + double? _pointsPossible; + double? get pointsPossible => _$this._pointsPossible; + set pointsPossible(double? pointsPossible) => _$this._pointsPossible = pointsPossible; - String _courseId; - String get courseId => _$this._courseId; - set courseId(String courseId) => _$this._courseId = courseId; + String? _courseId; + String? get courseId => _$this._courseId; + set courseId(String? courseId) => _$this._courseId = courseId; - GradingType _gradingType; - GradingType get gradingType => _$this._gradingType; - set gradingType(GradingType gradingType) => _$this._gradingType = gradingType; + GradingType? _gradingType; + GradingType? get gradingType => _$this._gradingType; + set gradingType(GradingType? gradingType) => + _$this._gradingType = gradingType; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - String _quizId; - String get quizId => _$this._quizId; - set quizId(String quizId) => _$this._quizId = quizId; + String? _quizId; + String? get quizId => _$this._quizId; + set quizId(String? quizId) => _$this._quizId = quizId; - bool _useRubricForGrading; - bool get useRubricForGrading => _$this._useRubricForGrading; - set useRubricForGrading(bool useRubricForGrading) => + bool? _useRubricForGrading; + bool? get useRubricForGrading => _$this._useRubricForGrading; + set useRubricForGrading(bool? useRubricForGrading) => _$this._useRubricForGrading = useRubricForGrading; - SubmissionWrapperBuilder _submissionWrapper; + SubmissionWrapperBuilder? _submissionWrapper; SubmissionWrapperBuilder get submissionWrapper => _$this._submissionWrapper ??= new SubmissionWrapperBuilder(); - set submissionWrapper(SubmissionWrapperBuilder submissionWrapper) => + set submissionWrapper(SubmissionWrapperBuilder? submissionWrapper) => _$this._submissionWrapper = submissionWrapper; - String _assignmentGroupId; - String get assignmentGroupId => _$this._assignmentGroupId; - set assignmentGroupId(String assignmentGroupId) => + String? _assignmentGroupId; + String? get assignmentGroupId => _$this._assignmentGroupId; + set assignmentGroupId(String? assignmentGroupId) => _$this._assignmentGroupId = assignmentGroupId; - int _position; - int get position => _$this._position; - set position(int position) => _$this._position = position; + int? _position; + int? get position => _$this._position; + set position(int? position) => _$this._position = position; - LockInfoBuilder _lockInfo; + LockInfoBuilder? _lockInfo; LockInfoBuilder get lockInfo => _$this._lockInfo ??= new LockInfoBuilder(); - set lockInfo(LockInfoBuilder lockInfo) => _$this._lockInfo = lockInfo; + set lockInfo(LockInfoBuilder? lockInfo) => _$this._lockInfo = lockInfo; - bool _lockedForUser; - bool get lockedForUser => _$this._lockedForUser; - set lockedForUser(bool lockedForUser) => + bool? _lockedForUser; + bool? get lockedForUser => _$this._lockedForUser; + set lockedForUser(bool? lockedForUser) => _$this._lockedForUser = lockedForUser; - DateTime _lockAt; - DateTime get lockAt => _$this._lockAt; - set lockAt(DateTime lockAt) => _$this._lockAt = lockAt; + DateTime? _lockAt; + DateTime? get lockAt => _$this._lockAt; + set lockAt(DateTime? lockAt) => _$this._lockAt = lockAt; - DateTime _unlockAt; - DateTime get unlockAt => _$this._unlockAt; - set unlockAt(DateTime unlockAt) => _$this._unlockAt = unlockAt; + DateTime? _unlockAt; + DateTime? get unlockAt => _$this._unlockAt; + set unlockAt(DateTime? unlockAt) => _$this._unlockAt = unlockAt; - String _lockExplanation; - String get lockExplanation => _$this._lockExplanation; - set lockExplanation(String lockExplanation) => + String? _lockExplanation; + String? get lockExplanation => _$this._lockExplanation; + set lockExplanation(String? lockExplanation) => _$this._lockExplanation = lockExplanation; - bool _freeFormCriterionComments; - bool get freeFormCriterionComments => _$this._freeFormCriterionComments; - set freeFormCriterionComments(bool freeFormCriterionComments) => + bool? _freeFormCriterionComments; + bool? get freeFormCriterionComments => _$this._freeFormCriterionComments; + set freeFormCriterionComments(bool? freeFormCriterionComments) => _$this._freeFormCriterionComments = freeFormCriterionComments; - bool _published; - bool get published => _$this._published; - set published(bool published) => _$this._published = published; + bool? _published; + bool? get published => _$this._published; + set published(bool? published) => _$this._published = published; - String _groupCategoryId; - String get groupCategoryId => _$this._groupCategoryId; - set groupCategoryId(String groupCategoryId) => + String? _groupCategoryId; + String? get groupCategoryId => _$this._groupCategoryId; + set groupCategoryId(String? groupCategoryId) => _$this._groupCategoryId = groupCategoryId; - bool _userSubmitted; - bool get userSubmitted => _$this._userSubmitted; - set userSubmitted(bool userSubmitted) => + bool? _userSubmitted; + bool? get userSubmitted => _$this._userSubmitted; + set userSubmitted(bool? userSubmitted) => _$this._userSubmitted = userSubmitted; - bool _onlyVisibleToOverrides; - bool get onlyVisibleToOverrides => _$this._onlyVisibleToOverrides; - set onlyVisibleToOverrides(bool onlyVisibleToOverrides) => + bool? _onlyVisibleToOverrides; + bool? get onlyVisibleToOverrides => _$this._onlyVisibleToOverrides; + set onlyVisibleToOverrides(bool? onlyVisibleToOverrides) => _$this._onlyVisibleToOverrides = onlyVisibleToOverrides; - bool _anonymousPeerReviews; - bool get anonymousPeerReviews => _$this._anonymousPeerReviews; - set anonymousPeerReviews(bool anonymousPeerReviews) => + bool? _anonymousPeerReviews; + bool? get anonymousPeerReviews => _$this._anonymousPeerReviews; + set anonymousPeerReviews(bool? anonymousPeerReviews) => _$this._anonymousPeerReviews = anonymousPeerReviews; - bool _moderatedGrading; - bool get moderatedGrading => _$this._moderatedGrading; - set moderatedGrading(bool moderatedGrading) => + bool? _moderatedGrading; + bool? get moderatedGrading => _$this._moderatedGrading; + set moderatedGrading(bool? moderatedGrading) => _$this._moderatedGrading = moderatedGrading; - bool _anonymousGrading; - bool get anonymousGrading => _$this._anonymousGrading; - set anonymousGrading(bool anonymousGrading) => + bool? _anonymousGrading; + bool? get anonymousGrading => _$this._anonymousGrading; + set anonymousGrading(bool? anonymousGrading) => _$this._anonymousGrading = anonymousGrading; - bool _isStudioEnabled; - bool get isStudioEnabled => _$this._isStudioEnabled; - set isStudioEnabled(bool isStudioEnabled) => + bool? _isStudioEnabled; + bool? get isStudioEnabled => _$this._isStudioEnabled; + set isStudioEnabled(bool? isStudioEnabled) => _$this._isStudioEnabled = isStudioEnabled; - ListBuilder _submissionTypes; + ListBuilder? _submissionTypes; ListBuilder get submissionTypes => _$this._submissionTypes ??= new ListBuilder(); - set submissionTypes(ListBuilder submissionTypes) => + set submissionTypes(ListBuilder? submissionTypes) => _$this._submissionTypes = submissionTypes; AssignmentBuilder() { @@ -878,36 +843,37 @@ class AssignmentBuilder implements Builder { } AssignmentBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _description = _$v.description; - _dueAt = _$v.dueAt; - _pointsPossible = _$v.pointsPossible; - _courseId = _$v.courseId; - _gradingType = _$v.gradingType; - _htmlUrl = _$v.htmlUrl; - _url = _$v.url; - _quizId = _$v.quizId; - _useRubricForGrading = _$v.useRubricForGrading; - _submissionWrapper = _$v.submissionWrapper?.toBuilder(); - _assignmentGroupId = _$v.assignmentGroupId; - _position = _$v.position; - _lockInfo = _$v.lockInfo?.toBuilder(); - _lockedForUser = _$v.lockedForUser; - _lockAt = _$v.lockAt; - _unlockAt = _$v.unlockAt; - _lockExplanation = _$v.lockExplanation; - _freeFormCriterionComments = _$v.freeFormCriterionComments; - _published = _$v.published; - _groupCategoryId = _$v.groupCategoryId; - _userSubmitted = _$v.userSubmitted; - _onlyVisibleToOverrides = _$v.onlyVisibleToOverrides; - _anonymousPeerReviews = _$v.anonymousPeerReviews; - _moderatedGrading = _$v.moderatedGrading; - _anonymousGrading = _$v.anonymousGrading; - _isStudioEnabled = _$v.isStudioEnabled; - _submissionTypes = _$v.submissionTypes?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _description = $v.description; + _dueAt = $v.dueAt; + _pointsPossible = $v.pointsPossible; + _courseId = $v.courseId; + _gradingType = $v.gradingType; + _htmlUrl = $v.htmlUrl; + _url = $v.url; + _quizId = $v.quizId; + _useRubricForGrading = $v.useRubricForGrading; + _submissionWrapper = $v.submissionWrapper?.toBuilder(); + _assignmentGroupId = $v.assignmentGroupId; + _position = $v.position; + _lockInfo = $v.lockInfo?.toBuilder(); + _lockedForUser = $v.lockedForUser; + _lockAt = $v.lockAt; + _unlockAt = $v.unlockAt; + _lockExplanation = $v.lockExplanation; + _freeFormCriterionComments = $v.freeFormCriterionComments; + _published = $v.published; + _groupCategoryId = $v.groupCategoryId; + _userSubmitted = $v.userSubmitted; + _onlyVisibleToOverrides = $v.onlyVisibleToOverrides; + _anonymousPeerReviews = $v.anonymousPeerReviews; + _moderatedGrading = $v.moderatedGrading; + _anonymousGrading = $v.anonymousGrading; + _isStudioEnabled = $v.isStudioEnabled; + _submissionTypes = $v.submissionTypes?.toBuilder(); _$v = null; } return this; @@ -915,66 +881,76 @@ class AssignmentBuilder implements Builder { @override void replace(Assignment other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Assignment; } @override - void update(void Function(AssignmentBuilder) updates) { + void update(void Function(AssignmentBuilder)? updates) { if (updates != null) updates(this); } @override - _$Assignment build() { + Assignment build() => _build(); + + _$Assignment _build() { _$Assignment _$result; try { _$result = _$v ?? new _$Assignment._( - id: id, + id: BuiltValueNullFieldError.checkNotNull( + id, r'Assignment', 'id'), name: name, description: description, dueAt: dueAt, - pointsPossible: pointsPossible, - courseId: courseId, + pointsPossible: BuiltValueNullFieldError.checkNotNull( + pointsPossible, r'Assignment', 'pointsPossible'), + courseId: BuiltValueNullFieldError.checkNotNull( + courseId, r'Assignment', 'courseId'), gradingType: gradingType, htmlUrl: htmlUrl, url: url, quizId: quizId, - useRubricForGrading: useRubricForGrading, + useRubricForGrading: BuiltValueNullFieldError.checkNotNull( + useRubricForGrading, r'Assignment', 'useRubricForGrading'), submissionWrapper: _submissionWrapper?.build(), - assignmentGroupId: assignmentGroupId, - position: position, - lockInfo: lockInfo.build(), - lockedForUser: lockedForUser, + assignmentGroupId: BuiltValueNullFieldError.checkNotNull( + assignmentGroupId, r'Assignment', 'assignmentGroupId'), + position: BuiltValueNullFieldError.checkNotNull( + position, r'Assignment', 'position'), + lockInfo: _lockInfo?.build(), + lockedForUser: BuiltValueNullFieldError.checkNotNull( + lockedForUser, r'Assignment', 'lockedForUser'), lockAt: lockAt, unlockAt: unlockAt, lockExplanation: lockExplanation, - freeFormCriterionComments: freeFormCriterionComments, - published: published, + freeFormCriterionComments: BuiltValueNullFieldError.checkNotNull( + freeFormCriterionComments, + r'Assignment', + 'freeFormCriterionComments'), + published: BuiltValueNullFieldError.checkNotNull(published, r'Assignment', 'published'), groupCategoryId: groupCategoryId, - userSubmitted: userSubmitted, - onlyVisibleToOverrides: onlyVisibleToOverrides, - anonymousPeerReviews: anonymousPeerReviews, - moderatedGrading: moderatedGrading, - anonymousGrading: anonymousGrading, - isStudioEnabled: isStudioEnabled, + userSubmitted: BuiltValueNullFieldError.checkNotNull(userSubmitted, r'Assignment', 'userSubmitted'), + onlyVisibleToOverrides: BuiltValueNullFieldError.checkNotNull(onlyVisibleToOverrides, r'Assignment', 'onlyVisibleToOverrides'), + anonymousPeerReviews: BuiltValueNullFieldError.checkNotNull(anonymousPeerReviews, r'Assignment', 'anonymousPeerReviews'), + moderatedGrading: BuiltValueNullFieldError.checkNotNull(moderatedGrading, r'Assignment', 'moderatedGrading'), + anonymousGrading: BuiltValueNullFieldError.checkNotNull(anonymousGrading, r'Assignment', 'anonymousGrading'), + isStudioEnabled: BuiltValueNullFieldError.checkNotNull(isStudioEnabled, r'Assignment', 'isStudioEnabled'), submissionTypes: _submissionTypes?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'submissionWrapper'; _submissionWrapper?.build(); _$failedField = 'lockInfo'; - lockInfo.build(); + _lockInfo?.build(); _$failedField = 'submissionTypes'; _submissionTypes?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Assignment', _$failedField, e.toString()); + r'Assignment', _$failedField, e.toString()); } rethrow; } @@ -983,4 +959,4 @@ class AssignmentBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/assignment_group.g.dart b/apps/flutter_parent/lib/models/assignment_group.g.dart index 70cac31006..9fe4fce4f7 100644 --- a/apps/flutter_parent/lib/models/assignment_group.g.dart +++ b/apps/flutter_parent/lib/models/assignment_group.g.dart @@ -17,9 +17,9 @@ class _$AssignmentGroupSerializer final String wireName = 'AssignmentGroup'; @override - Iterable serialize(Serializers serializers, AssignmentGroup object, + Iterable serialize(Serializers serializers, AssignmentGroup object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', @@ -41,38 +41,37 @@ class _$AssignmentGroupSerializer @override AssignmentGroup deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AssignmentGroupBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'position': result.position = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; case 'group_weight': result.groupWeight = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double))! as double; break; case 'assignments': result.assignments.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Assignment)])) - as BuiltList); + BuiltList, const [const FullType(Assignment)]))! + as BuiltList); break; } } @@ -93,27 +92,24 @@ class _$AssignmentGroup extends AssignmentGroup { @override final BuiltList assignments; - factory _$AssignmentGroup([void Function(AssignmentGroupBuilder) updates]) => - (new AssignmentGroupBuilder()..update(updates)).build(); + factory _$AssignmentGroup([void Function(AssignmentGroupBuilder)? updates]) => + (new AssignmentGroupBuilder()..update(updates))._build(); _$AssignmentGroup._( - {this.id, this.name, this.position, this.groupWeight, this.assignments}) + {required this.id, + required this.name, + required this.position, + required this.groupWeight, + required this.assignments}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('AssignmentGroup', 'id'); - } - if (name == null) { - throw new BuiltValueNullFieldError('AssignmentGroup', 'name'); - } - if (position == null) { - throw new BuiltValueNullFieldError('AssignmentGroup', 'position'); - } - if (groupWeight == null) { - throw new BuiltValueNullFieldError('AssignmentGroup', 'groupWeight'); - } - if (assignments == null) { - throw new BuiltValueNullFieldError('AssignmentGroup', 'assignments'); - } + BuiltValueNullFieldError.checkNotNull(id, r'AssignmentGroup', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'AssignmentGroup', 'name'); + BuiltValueNullFieldError.checkNotNull( + position, r'AssignmentGroup', 'position'); + BuiltValueNullFieldError.checkNotNull( + groupWeight, r'AssignmentGroup', 'groupWeight'); + BuiltValueNullFieldError.checkNotNull( + assignments, r'AssignmentGroup', 'assignments'); } @override @@ -137,15 +133,19 @@ class _$AssignmentGroup extends AssignmentGroup { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), name.hashCode), position.hashCode), - groupWeight.hashCode), - assignments.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, position.hashCode); + _$hash = $jc(_$hash, groupWeight.hashCode); + _$hash = $jc(_$hash, assignments.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('AssignmentGroup') + return (newBuiltValueToStringHelper(r'AssignmentGroup') ..add('id', id) ..add('name', name) ..add('position', position) @@ -157,28 +157,28 @@ class _$AssignmentGroup extends AssignmentGroup { class AssignmentGroupBuilder implements Builder { - _$AssignmentGroup _$v; + _$AssignmentGroup? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - int _position; - int get position => _$this._position; - set position(int position) => _$this._position = position; + int? _position; + int? get position => _$this._position; + set position(int? position) => _$this._position = position; - double _groupWeight; - double get groupWeight => _$this._groupWeight; - set groupWeight(double groupWeight) => _$this._groupWeight = groupWeight; + double? _groupWeight; + double? get groupWeight => _$this._groupWeight; + set groupWeight(double? groupWeight) => _$this._groupWeight = groupWeight; - ListBuilder _assignments; + ListBuilder? _assignments; ListBuilder get assignments => _$this._assignments ??= new ListBuilder(); - set assignments(ListBuilder assignments) => + set assignments(ListBuilder? assignments) => _$this._assignments = assignments; AssignmentGroupBuilder() { @@ -186,12 +186,13 @@ class AssignmentGroupBuilder } AssignmentGroupBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _position = _$v.position; - _groupWeight = _$v.groupWeight; - _assignments = _$v.assignments?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _position = $v.position; + _groupWeight = $v.groupWeight; + _assignments = $v.assignments.toBuilder(); _$v = null; } return this; @@ -199,36 +200,40 @@ class AssignmentGroupBuilder @override void replace(AssignmentGroup other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$AssignmentGroup; } @override - void update(void Function(AssignmentGroupBuilder) updates) { + void update(void Function(AssignmentGroupBuilder)? updates) { if (updates != null) updates(this); } @override - _$AssignmentGroup build() { + AssignmentGroup build() => _build(); + + _$AssignmentGroup _build() { _$AssignmentGroup _$result; try { _$result = _$v ?? new _$AssignmentGroup._( - id: id, - name: name, - position: position, - groupWeight: groupWeight, + id: BuiltValueNullFieldError.checkNotNull( + id, r'AssignmentGroup', 'id'), + name: BuiltValueNullFieldError.checkNotNull( + name, r'AssignmentGroup', 'name'), + position: BuiltValueNullFieldError.checkNotNull( + position, r'AssignmentGroup', 'position'), + groupWeight: BuiltValueNullFieldError.checkNotNull( + groupWeight, r'AssignmentGroup', 'groupWeight'), assignments: assignments.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'assignments'; assignments.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'AssignmentGroup', _$failedField, e.toString()); + r'AssignmentGroup', _$failedField, e.toString()); } rethrow; } @@ -237,4 +242,4 @@ class AssignmentGroupBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/assignment_override.dart b/apps/flutter_parent/lib/models/assignment_override.dart index 62ad75577c..1736192c1e 100644 --- a/apps/flutter_parent/lib/models/assignment_override.dart +++ b/apps/flutter_parent/lib/models/assignment_override.dart @@ -31,28 +31,22 @@ abstract class AssignmentOverride implements Built get studentIds; diff --git a/apps/flutter_parent/lib/models/assignment_override.g.dart b/apps/flutter_parent/lib/models/assignment_override.g.dart index 992f0080ac..4c8bcc6e64 100644 --- a/apps/flutter_parent/lib/models/assignment_override.g.dart +++ b/apps/flutter_parent/lib/models/assignment_override.g.dart @@ -17,9 +17,10 @@ class _$AssignmentOverrideSerializer final String wireName = 'AssignmentOverride'; @override - Iterable serialize(Serializers serializers, AssignmentOverride object, + Iterable serialize( + Serializers serializers, AssignmentOverride object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'assignment_id', @@ -30,101 +31,95 @@ class _$AssignmentOverrideSerializer specifiedType: const FullType(BuiltList, const [const FullType(String)])), ]; - result.add('title'); - if (object.title == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.title, - specifiedType: const FullType(String))); - } - result.add('due_at'); - if (object.dueAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.dueAt, + Object? value; + value = object.title; + + result + ..add('title') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.dueAt; + + result + ..add('due_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('all_day'); - if (object.allDay == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.allDay, - specifiedType: const FullType(bool))); - } - result.add('all_day_date'); - if (object.allDayDate == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.allDayDate, + value = object.allDay; + + result + ..add('all_day') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.allDayDate; + + result + ..add('all_day_date') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('unlock_at'); - if (object.unlockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.unlockAt, + value = object.unlockAt; + + result + ..add('unlock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('lock_at'); - if (object.lockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockAt, + value = object.lockAt; + + result + ..add('lock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override AssignmentOverride deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AssignmentOverrideBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'assignment_id': result.assignmentId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'due_at': result.dueAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'all_day': result.allDay = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'all_day_date': result.allDayDate = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'unlock_at': result.unlockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'lock_at': result.lockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'student_ids': result.studentIds.replace(serializers.deserialize(value, - specifiedType: - const FullType(BuiltList, const [const FullType(String)])) - as BuiltList); + specifiedType: const FullType( + BuiltList, const [const FullType(String)]))! + as BuiltList); break; } } @@ -139,44 +134,40 @@ class _$AssignmentOverride extends AssignmentOverride { @override final String assignmentId; @override - final String title; + final String? title; @override - final DateTime dueAt; + final DateTime? dueAt; @override - final bool allDay; + final bool? allDay; @override - final DateTime allDayDate; + final DateTime? allDayDate; @override - final DateTime unlockAt; + final DateTime? unlockAt; @override - final DateTime lockAt; + final DateTime? lockAt; @override final BuiltList studentIds; factory _$AssignmentOverride( - [void Function(AssignmentOverrideBuilder) updates]) => - (new AssignmentOverrideBuilder()..update(updates)).build(); + [void Function(AssignmentOverrideBuilder)? updates]) => + (new AssignmentOverrideBuilder()..update(updates))._build(); _$AssignmentOverride._( - {this.id, - this.assignmentId, + {required this.id, + required this.assignmentId, this.title, this.dueAt, this.allDay, this.allDayDate, this.unlockAt, this.lockAt, - this.studentIds}) + required this.studentIds}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('AssignmentOverride', 'id'); - } - if (assignmentId == null) { - throw new BuiltValueNullFieldError('AssignmentOverride', 'assignmentId'); - } - if (studentIds == null) { - throw new BuiltValueNullFieldError('AssignmentOverride', 'studentIds'); - } + BuiltValueNullFieldError.checkNotNull(id, r'AssignmentOverride', 'id'); + BuiltValueNullFieldError.checkNotNull( + assignmentId, r'AssignmentOverride', 'assignmentId'); + BuiltValueNullFieldError.checkNotNull( + studentIds, r'AssignmentOverride', 'studentIds'); } @override @@ -205,25 +196,23 @@ class _$AssignmentOverride extends AssignmentOverride { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), assignmentId.hashCode), - title.hashCode), - dueAt.hashCode), - allDay.hashCode), - allDayDate.hashCode), - unlockAt.hashCode), - lockAt.hashCode), - studentIds.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, assignmentId.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, dueAt.hashCode); + _$hash = $jc(_$hash, allDay.hashCode); + _$hash = $jc(_$hash, allDayDate.hashCode); + _$hash = $jc(_$hash, unlockAt.hashCode); + _$hash = $jc(_$hash, lockAt.hashCode); + _$hash = $jc(_$hash, studentIds.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('AssignmentOverride') + return (newBuiltValueToStringHelper(r'AssignmentOverride') ..add('id', id) ..add('assignmentId', assignmentId) ..add('title', title) @@ -239,44 +228,44 @@ class _$AssignmentOverride extends AssignmentOverride { class AssignmentOverrideBuilder implements Builder { - _$AssignmentOverride _$v; + _$AssignmentOverride? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _assignmentId; - String get assignmentId => _$this._assignmentId; - set assignmentId(String assignmentId) => _$this._assignmentId = assignmentId; + String? _assignmentId; + String? get assignmentId => _$this._assignmentId; + set assignmentId(String? assignmentId) => _$this._assignmentId = assignmentId; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - DateTime _dueAt; - DateTime get dueAt => _$this._dueAt; - set dueAt(DateTime dueAt) => _$this._dueAt = dueAt; + DateTime? _dueAt; + DateTime? get dueAt => _$this._dueAt; + set dueAt(DateTime? dueAt) => _$this._dueAt = dueAt; - bool _allDay; - bool get allDay => _$this._allDay; - set allDay(bool allDay) => _$this._allDay = allDay; + bool? _allDay; + bool? get allDay => _$this._allDay; + set allDay(bool? allDay) => _$this._allDay = allDay; - DateTime _allDayDate; - DateTime get allDayDate => _$this._allDayDate; - set allDayDate(DateTime allDayDate) => _$this._allDayDate = allDayDate; + DateTime? _allDayDate; + DateTime? get allDayDate => _$this._allDayDate; + set allDayDate(DateTime? allDayDate) => _$this._allDayDate = allDayDate; - DateTime _unlockAt; - DateTime get unlockAt => _$this._unlockAt; - set unlockAt(DateTime unlockAt) => _$this._unlockAt = unlockAt; + DateTime? _unlockAt; + DateTime? get unlockAt => _$this._unlockAt; + set unlockAt(DateTime? unlockAt) => _$this._unlockAt = unlockAt; - DateTime _lockAt; - DateTime get lockAt => _$this._lockAt; - set lockAt(DateTime lockAt) => _$this._lockAt = lockAt; + DateTime? _lockAt; + DateTime? get lockAt => _$this._lockAt; + set lockAt(DateTime? lockAt) => _$this._lockAt = lockAt; - ListBuilder _studentIds; + ListBuilder? _studentIds; ListBuilder get studentIds => _$this._studentIds ??= new ListBuilder(); - set studentIds(ListBuilder studentIds) => + set studentIds(ListBuilder? studentIds) => _$this._studentIds = studentIds; AssignmentOverrideBuilder() { @@ -284,16 +273,17 @@ class AssignmentOverrideBuilder } AssignmentOverrideBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _assignmentId = _$v.assignmentId; - _title = _$v.title; - _dueAt = _$v.dueAt; - _allDay = _$v.allDay; - _allDayDate = _$v.allDayDate; - _unlockAt = _$v.unlockAt; - _lockAt = _$v.lockAt; - _studentIds = _$v.studentIds?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _assignmentId = $v.assignmentId; + _title = $v.title; + _dueAt = $v.dueAt; + _allDay = $v.allDay; + _allDayDate = $v.allDayDate; + _unlockAt = $v.unlockAt; + _lockAt = $v.lockAt; + _studentIds = $v.studentIds.toBuilder(); _$v = null; } return this; @@ -301,25 +291,27 @@ class AssignmentOverrideBuilder @override void replace(AssignmentOverride other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$AssignmentOverride; } @override - void update(void Function(AssignmentOverrideBuilder) updates) { + void update(void Function(AssignmentOverrideBuilder)? updates) { if (updates != null) updates(this); } @override - _$AssignmentOverride build() { + AssignmentOverride build() => _build(); + + _$AssignmentOverride _build() { _$AssignmentOverride _$result; try { _$result = _$v ?? new _$AssignmentOverride._( - id: id, - assignmentId: assignmentId, + id: BuiltValueNullFieldError.checkNotNull( + id, r'AssignmentOverride', 'id'), + assignmentId: BuiltValueNullFieldError.checkNotNull( + assignmentId, r'AssignmentOverride', 'assignmentId'), title: title, dueAt: dueAt, allDay: allDay, @@ -328,13 +320,13 @@ class AssignmentOverrideBuilder lockAt: lockAt, studentIds: studentIds.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'studentIds'; studentIds.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'AssignmentOverride', _$failedField, e.toString()); + r'AssignmentOverride', _$failedField, e.toString()); } rethrow; } @@ -343,4 +335,4 @@ class AssignmentOverrideBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/attachment.dart b/apps/flutter_parent/lib/models/attachment.dart index 16f380dabf..deeb0806ee 100644 --- a/apps/flutter_parent/lib/models/attachment.dart +++ b/apps/flutter_parent/lib/models/attachment.dart @@ -32,38 +32,31 @@ abstract class Attachment implements Built { JsonObject get jsonId; @BuiltValueField(wireName: 'content-type') - @nullable - String get contentType; + String? get contentType; - @nullable - String get filename; + String? get filename; @BuiltValueField(wireName: 'display_name') - @nullable - String get displayName; + String? get displayName; - @nullable - String get url; + String? get url; @BuiltValueField(wireName: 'thumbnail_url') - @nullable - String get thumbnailUrl; + String? get thumbnailUrl; @BuiltValueField(wireName: 'preview_url') - @nullable - String get previewUrl; + String? get previewUrl; @BuiltValueField(wireName: 'created_at') - @nullable - DateTime get createdAt; + DateTime? get createdAt; int get size; - String inferContentType() { - if (contentType != null && contentType.isNotEmpty) return contentType; + String? inferContentType() { + if (contentType != null && contentType?.isNotEmpty == true) return contentType!; // First, attempt to infer content type from file name - String type = lookupMimeType(filename ?? ''); + String? type = lookupMimeType(filename ?? ''); // Next, attempt to infer from url if (type == null) type = lookupMimeType(url ?? ''); diff --git a/apps/flutter_parent/lib/models/attachment.g.dart b/apps/flutter_parent/lib/models/attachment.g.dart index a9215da050..4535d3beac 100644 --- a/apps/flutter_parent/lib/models/attachment.g.dart +++ b/apps/flutter_parent/lib/models/attachment.g.dart @@ -15,114 +15,108 @@ class _$AttachmentSerializer implements StructuredSerializer { final String wireName = 'Attachment'; @override - Iterable serialize(Serializers serializers, Attachment object, + Iterable serialize(Serializers serializers, Attachment object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.jsonId, specifiedType: const FullType(JsonObject)), 'size', serializers.serialize(object.size, specifiedType: const FullType(int)), ]; - result.add('content-type'); - if (object.contentType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contentType, - specifiedType: const FullType(String))); - } - result.add('filename'); - if (object.filename == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.filename, - specifiedType: const FullType(String))); - } - result.add('display_name'); - if (object.displayName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.displayName, - specifiedType: const FullType(String))); - } - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('thumbnail_url'); - if (object.thumbnailUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.thumbnailUrl, - specifiedType: const FullType(String))); - } - result.add('preview_url'); - if (object.previewUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.previewUrl, - specifiedType: const FullType(String))); - } - result.add('created_at'); - if (object.createdAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.createdAt, + Object? value; + value = object.contentType; + + result + ..add('content-type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.filename; + + result + ..add('filename') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.displayName; + + result + ..add('display_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.thumbnailUrl; + + result + ..add('thumbnail_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.previewUrl; + + result + ..add('preview_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.createdAt; + + result + ..add('created_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override - Attachment deserialize(Serializers serializers, Iterable serialized, + Attachment deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AttachmentBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.jsonId = serializers.deserialize(value, - specifiedType: const FullType(JsonObject)) as JsonObject; + specifiedType: const FullType(JsonObject))! as JsonObject; break; case 'content-type': result.contentType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'filename': result.filename = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'display_name': result.displayName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'thumbnail_url': result.thumbnailUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'preview_url': result.previewUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'created_at': result.createdAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'size': result.size = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; } } @@ -135,27 +129,27 @@ class _$Attachment extends Attachment { @override final JsonObject jsonId; @override - final String contentType; + final String? contentType; @override - final String filename; + final String? filename; @override - final String displayName; + final String? displayName; @override - final String url; + final String? url; @override - final String thumbnailUrl; + final String? thumbnailUrl; @override - final String previewUrl; + final String? previewUrl; @override - final DateTime createdAt; + final DateTime? createdAt; @override final int size; - factory _$Attachment([void Function(AttachmentBuilder) updates]) => - (new AttachmentBuilder()..update(updates)).build(); + factory _$Attachment([void Function(AttachmentBuilder)? updates]) => + (new AttachmentBuilder()..update(updates))._build(); _$Attachment._( - {this.jsonId, + {required this.jsonId, this.contentType, this.filename, this.displayName, @@ -163,14 +157,10 @@ class _$Attachment extends Attachment { this.thumbnailUrl, this.previewUrl, this.createdAt, - this.size}) + required this.size}) : super._() { - if (jsonId == null) { - throw new BuiltValueNullFieldError('Attachment', 'jsonId'); - } - if (size == null) { - throw new BuiltValueNullFieldError('Attachment', 'size'); - } + BuiltValueNullFieldError.checkNotNull(jsonId, r'Attachment', 'jsonId'); + BuiltValueNullFieldError.checkNotNull(size, r'Attachment', 'size'); } @override @@ -197,27 +187,23 @@ class _$Attachment extends Attachment { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc(0, jsonId.hashCode), - contentType.hashCode), - filename.hashCode), - displayName.hashCode), - url.hashCode), - thumbnailUrl.hashCode), - previewUrl.hashCode), - createdAt.hashCode), - size.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, jsonId.hashCode); + _$hash = $jc(_$hash, contentType.hashCode); + _$hash = $jc(_$hash, filename.hashCode); + _$hash = $jc(_$hash, displayName.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, thumbnailUrl.hashCode); + _$hash = $jc(_$hash, previewUrl.hashCode); + _$hash = $jc(_$hash, createdAt.hashCode); + _$hash = $jc(_$hash, size.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Attachment') + return (newBuiltValueToStringHelper(r'Attachment') ..add('jsonId', jsonId) ..add('contentType', contentType) ..add('filename', filename) @@ -232,59 +218,60 @@ class _$Attachment extends Attachment { } class AttachmentBuilder implements Builder { - _$Attachment _$v; + _$Attachment? _$v; - JsonObject _jsonId; - JsonObject get jsonId => _$this._jsonId; - set jsonId(JsonObject jsonId) => _$this._jsonId = jsonId; + JsonObject? _jsonId; + JsonObject? get jsonId => _$this._jsonId; + set jsonId(JsonObject? jsonId) => _$this._jsonId = jsonId; - String _contentType; - String get contentType => _$this._contentType; - set contentType(String contentType) => _$this._contentType = contentType; + String? _contentType; + String? get contentType => _$this._contentType; + set contentType(String? contentType) => _$this._contentType = contentType; - String _filename; - String get filename => _$this._filename; - set filename(String filename) => _$this._filename = filename; + String? _filename; + String? get filename => _$this._filename; + set filename(String? filename) => _$this._filename = filename; - String _displayName; - String get displayName => _$this._displayName; - set displayName(String displayName) => _$this._displayName = displayName; + String? _displayName; + String? get displayName => _$this._displayName; + set displayName(String? displayName) => _$this._displayName = displayName; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - String _thumbnailUrl; - String get thumbnailUrl => _$this._thumbnailUrl; - set thumbnailUrl(String thumbnailUrl) => _$this._thumbnailUrl = thumbnailUrl; + String? _thumbnailUrl; + String? get thumbnailUrl => _$this._thumbnailUrl; + set thumbnailUrl(String? thumbnailUrl) => _$this._thumbnailUrl = thumbnailUrl; - String _previewUrl; - String get previewUrl => _$this._previewUrl; - set previewUrl(String previewUrl) => _$this._previewUrl = previewUrl; + String? _previewUrl; + String? get previewUrl => _$this._previewUrl; + set previewUrl(String? previewUrl) => _$this._previewUrl = previewUrl; - DateTime _createdAt; - DateTime get createdAt => _$this._createdAt; - set createdAt(DateTime createdAt) => _$this._createdAt = createdAt; + DateTime? _createdAt; + DateTime? get createdAt => _$this._createdAt; + set createdAt(DateTime? createdAt) => _$this._createdAt = createdAt; - int _size; - int get size => _$this._size; - set size(int size) => _$this._size = size; + int? _size; + int? get size => _$this._size; + set size(int? size) => _$this._size = size; AttachmentBuilder() { Attachment._initializeBuilder(this); } AttachmentBuilder get _$this { - if (_$v != null) { - _jsonId = _$v.jsonId; - _contentType = _$v.contentType; - _filename = _$v.filename; - _displayName = _$v.displayName; - _url = _$v.url; - _thumbnailUrl = _$v.thumbnailUrl; - _previewUrl = _$v.previewUrl; - _createdAt = _$v.createdAt; - _size = _$v.size; + final $v = _$v; + if ($v != null) { + _jsonId = $v.jsonId; + _contentType = $v.contentType; + _filename = $v.filename; + _displayName = $v.displayName; + _url = $v.url; + _thumbnailUrl = $v.thumbnailUrl; + _previewUrl = $v.previewUrl; + _createdAt = $v.createdAt; + _size = $v.size; _$v = null; } return this; @@ -292,22 +279,23 @@ class AttachmentBuilder implements Builder { @override void replace(Attachment other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Attachment; } @override - void update(void Function(AttachmentBuilder) updates) { + void update(void Function(AttachmentBuilder)? updates) { if (updates != null) updates(this); } @override - _$Attachment build() { + Attachment build() => _build(); + + _$Attachment _build() { final _$result = _$v ?? new _$Attachment._( - jsonId: jsonId, + jsonId: BuiltValueNullFieldError.checkNotNull( + jsonId, r'Attachment', 'jsonId'), contentType: contentType, filename: filename, displayName: displayName, @@ -315,10 +303,11 @@ class AttachmentBuilder implements Builder { thumbnailUrl: thumbnailUrl, previewUrl: previewUrl, createdAt: createdAt, - size: size); + size: BuiltValueNullFieldError.checkNotNull( + size, r'Attachment', 'size')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/authenticated_url.g.dart b/apps/flutter_parent/lib/models/authenticated_url.g.dart index 8cb06bd862..3f0c7e42f3 100644 --- a/apps/flutter_parent/lib/models/authenticated_url.g.dart +++ b/apps/flutter_parent/lib/models/authenticated_url.g.dart @@ -17,9 +17,9 @@ class _$AuthenticatedUrlSerializer final String wireName = 'AuthenticatedUrl'; @override - Iterable serialize(Serializers serializers, AuthenticatedUrl object, + Iterable serialize(Serializers serializers, AuthenticatedUrl object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'session_url', serializers.serialize(object.sessionUrl, specifiedType: const FullType(String)), @@ -33,23 +33,23 @@ class _$AuthenticatedUrlSerializer @override AuthenticatedUrl deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new AuthenticatedUrlBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'session_url': result.sessionUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'requires_terms_acceptance': result.requiresTermsAcceptance = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -65,15 +65,16 @@ class _$AuthenticatedUrl extends AuthenticatedUrl { final bool requiresTermsAcceptance; factory _$AuthenticatedUrl( - [void Function(AuthenticatedUrlBuilder) updates]) => - (new AuthenticatedUrlBuilder()..update(updates)).build(); + [void Function(AuthenticatedUrlBuilder)? updates]) => + (new AuthenticatedUrlBuilder()..update(updates))._build(); - _$AuthenticatedUrl._({this.sessionUrl, this.requiresTermsAcceptance}) + _$AuthenticatedUrl._( + {required this.sessionUrl, required this.requiresTermsAcceptance}) : super._() { BuiltValueNullFieldError.checkNotNull( - sessionUrl, 'AuthenticatedUrl', 'sessionUrl'); - BuiltValueNullFieldError.checkNotNull( - requiresTermsAcceptance, 'AuthenticatedUrl', 'requiresTermsAcceptance'); + sessionUrl, r'AuthenticatedUrl', 'sessionUrl'); + BuiltValueNullFieldError.checkNotNull(requiresTermsAcceptance, + r'AuthenticatedUrl', 'requiresTermsAcceptance'); } @override @@ -94,13 +95,16 @@ class _$AuthenticatedUrl extends AuthenticatedUrl { @override int get hashCode { - return $jf( - $jc($jc(0, sessionUrl.hashCode), requiresTermsAcceptance.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, sessionUrl.hashCode); + _$hash = $jc(_$hash, requiresTermsAcceptance.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('AuthenticatedUrl') + return (newBuiltValueToStringHelper(r'AuthenticatedUrl') ..add('sessionUrl', sessionUrl) ..add('requiresTermsAcceptance', requiresTermsAcceptance)) .toString(); @@ -109,15 +113,15 @@ class _$AuthenticatedUrl extends AuthenticatedUrl { class AuthenticatedUrlBuilder implements Builder { - _$AuthenticatedUrl _$v; + _$AuthenticatedUrl? _$v; - String _sessionUrl; - String get sessionUrl => _$this._sessionUrl; - set sessionUrl(String sessionUrl) => _$this._sessionUrl = sessionUrl; + String? _sessionUrl; + String? get sessionUrl => _$this._sessionUrl; + set sessionUrl(String? sessionUrl) => _$this._sessionUrl = sessionUrl; - bool _requiresTermsAcceptance; - bool get requiresTermsAcceptance => _$this._requiresTermsAcceptance; - set requiresTermsAcceptance(bool requiresTermsAcceptance) => + bool? _requiresTermsAcceptance; + bool? get requiresTermsAcceptance => _$this._requiresTermsAcceptance; + set requiresTermsAcceptance(bool? requiresTermsAcceptance) => _$this._requiresTermsAcceptance = requiresTermsAcceptance; AuthenticatedUrlBuilder() { @@ -141,23 +145,25 @@ class AuthenticatedUrlBuilder } @override - void update(void Function(AuthenticatedUrlBuilder) updates) { + void update(void Function(AuthenticatedUrlBuilder)? updates) { if (updates != null) updates(this); } @override - _$AuthenticatedUrl build() { + AuthenticatedUrl build() => _build(); + + _$AuthenticatedUrl _build() { final _$result = _$v ?? new _$AuthenticatedUrl._( sessionUrl: BuiltValueNullFieldError.checkNotNull( - sessionUrl, 'AuthenticatedUrl', 'sessionUrl'), + sessionUrl, r'AuthenticatedUrl', 'sessionUrl'), requiresTermsAcceptance: BuiltValueNullFieldError.checkNotNull( requiresTermsAcceptance, - 'AuthenticatedUrl', + r'AuthenticatedUrl', 'requiresTermsAcceptance')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/basic_user.dart b/apps/flutter_parent/lib/models/basic_user.dart index e0f067b0e5..38214c8945 100644 --- a/apps/flutter_parent/lib/models/basic_user.dart +++ b/apps/flutter_parent/lib/models/basic_user.dart @@ -25,15 +25,12 @@ abstract class BasicUser implements Built { String get id; - @nullable - String get name; + String? get name; - @nullable - String get pronouns; + String? get pronouns; @BuiltValueField(wireName: 'avatar_url') - @nullable - String get avatarUrl; + String? get avatarUrl; BasicUser._(); factory BasicUser([void Function(BasicUserBuilder) updates]) = _$BasicUser; diff --git a/apps/flutter_parent/lib/models/basic_user.g.dart b/apps/flutter_parent/lib/models/basic_user.g.dart index 857816b839..88338ecfff 100644 --- a/apps/flutter_parent/lib/models/basic_user.g.dart +++ b/apps/flutter_parent/lib/models/basic_user.g.dart @@ -15,63 +15,61 @@ class _$BasicUserSerializer implements StructuredSerializer { final String wireName = 'BasicUser'; @override - Iterable serialize(Serializers serializers, BasicUser object, + Iterable serialize(Serializers serializers, BasicUser object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), ]; - result.add('name'); - if (object.name == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.name, - specifiedType: const FullType(String))); - } - result.add('pronouns'); - if (object.pronouns == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.pronouns, - specifiedType: const FullType(String))); - } - result.add('avatar_url'); - if (object.avatarUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.avatarUrl, - specifiedType: const FullType(String))); - } + Object? value; + value = object.name; + + result + ..add('name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.pronouns; + + result + ..add('pronouns') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.avatarUrl; + + result + ..add('avatar_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - BasicUser deserialize(Serializers serializers, Iterable serialized, + BasicUser deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new BasicUserBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'pronouns': result.pronouns = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'avatar_url': result.avatarUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -84,20 +82,18 @@ class _$BasicUser extends BasicUser { @override final String id; @override - final String name; + final String? name; @override - final String pronouns; + final String? pronouns; @override - final String avatarUrl; + final String? avatarUrl; - factory _$BasicUser([void Function(BasicUserBuilder) updates]) => - (new BasicUserBuilder()..update(updates)).build(); + factory _$BasicUser([void Function(BasicUserBuilder)? updates]) => + (new BasicUserBuilder()..update(updates))._build(); - _$BasicUser._({this.id, this.name, this.pronouns, this.avatarUrl}) + _$BasicUser._({required this.id, this.name, this.pronouns, this.avatarUrl}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('BasicUser', 'id'); - } + BuiltValueNullFieldError.checkNotNull(id, r'BasicUser', 'id'); } @override @@ -119,14 +115,18 @@ class _$BasicUser extends BasicUser { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, id.hashCode), name.hashCode), pronouns.hashCode), - avatarUrl.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, pronouns.hashCode); + _$hash = $jc(_$hash, avatarUrl.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('BasicUser') + return (newBuiltValueToStringHelper(r'BasicUser') ..add('id', id) ..add('name', name) ..add('pronouns', pronouns) @@ -136,34 +136,35 @@ class _$BasicUser extends BasicUser { } class BasicUserBuilder implements Builder { - _$BasicUser _$v; + _$BasicUser? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _pronouns; - String get pronouns => _$this._pronouns; - set pronouns(String pronouns) => _$this._pronouns = pronouns; + String? _pronouns; + String? get pronouns => _$this._pronouns; + set pronouns(String? pronouns) => _$this._pronouns = pronouns; - String _avatarUrl; - String get avatarUrl => _$this._avatarUrl; - set avatarUrl(String avatarUrl) => _$this._avatarUrl = avatarUrl; + String? _avatarUrl; + String? get avatarUrl => _$this._avatarUrl; + set avatarUrl(String? avatarUrl) => _$this._avatarUrl = avatarUrl; BasicUserBuilder() { BasicUser._initializeBuilder(this); } BasicUserBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _pronouns = _$v.pronouns; - _avatarUrl = _$v.avatarUrl; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _pronouns = $v.pronouns; + _avatarUrl = $v.avatarUrl; _$v = null; } return this; @@ -171,25 +172,28 @@ class BasicUserBuilder implements Builder { @override void replace(BasicUser other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$BasicUser; } @override - void update(void Function(BasicUserBuilder) updates) { + void update(void Function(BasicUserBuilder)? updates) { if (updates != null) updates(this); } @override - _$BasicUser build() { + BasicUser build() => _build(); + + _$BasicUser _build() { final _$result = _$v ?? new _$BasicUser._( - id: id, name: name, pronouns: pronouns, avatarUrl: avatarUrl); + id: BuiltValueNullFieldError.checkNotNull(id, r'BasicUser', 'id'), + name: name, + pronouns: pronouns, + avatarUrl: avatarUrl); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/calendar_filter.dart b/apps/flutter_parent/lib/models/calendar_filter.dart index 06ec7cbd1e..0b1d03afa6 100644 --- a/apps/flutter_parent/lib/models/calendar_filter.dart +++ b/apps/flutter_parent/lib/models/calendar_filter.dart @@ -20,8 +20,7 @@ part 'calendar_filter.g.dart'; /// To have this built_value be generated, run this command from the project root: /// flutter pub run build_runner build --delete-conflicting-outputs abstract class CalendarFilter implements Built { - @nullable - int get id; + int? get id; String get userDomain; diff --git a/apps/flutter_parent/lib/models/calendar_filter.g.dart b/apps/flutter_parent/lib/models/calendar_filter.g.dart index 26e30671b7..3597758512 100644 --- a/apps/flutter_parent/lib/models/calendar_filter.g.dart +++ b/apps/flutter_parent/lib/models/calendar_filter.g.dart @@ -8,7 +8,7 @@ part of 'calendar_filter.dart'; class _$CalendarFilter extends CalendarFilter { @override - final int id; + final int? id; @override final String userDomain; @override @@ -18,24 +18,23 @@ class _$CalendarFilter extends CalendarFilter { @override final BuiltSet filters; - factory _$CalendarFilter([void Function(CalendarFilterBuilder) updates]) => - (new CalendarFilterBuilder()..update(updates)).build(); + factory _$CalendarFilter([void Function(CalendarFilterBuilder)? updates]) => + (new CalendarFilterBuilder()..update(updates))._build(); _$CalendarFilter._( - {this.id, this.userDomain, this.userId, this.observeeId, this.filters}) + {this.id, + required this.userDomain, + required this.userId, + required this.observeeId, + required this.filters}) : super._() { - if (userDomain == null) { - throw new BuiltValueNullFieldError('CalendarFilter', 'userDomain'); - } - if (userId == null) { - throw new BuiltValueNullFieldError('CalendarFilter', 'userId'); - } - if (observeeId == null) { - throw new BuiltValueNullFieldError('CalendarFilter', 'observeeId'); - } - if (filters == null) { - throw new BuiltValueNullFieldError('CalendarFilter', 'filters'); - } + BuiltValueNullFieldError.checkNotNull( + userDomain, r'CalendarFilter', 'userDomain'); + BuiltValueNullFieldError.checkNotNull(userId, r'CalendarFilter', 'userId'); + BuiltValueNullFieldError.checkNotNull( + observeeId, r'CalendarFilter', 'observeeId'); + BuiltValueNullFieldError.checkNotNull( + filters, r'CalendarFilter', 'filters'); } @override @@ -59,15 +58,19 @@ class _$CalendarFilter extends CalendarFilter { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), userDomain.hashCode), userId.hashCode), - observeeId.hashCode), - filters.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, userDomain.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, observeeId.hashCode); + _$hash = $jc(_$hash, filters.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CalendarFilter') + return (newBuiltValueToStringHelper(r'CalendarFilter') ..add('id', id) ..add('userDomain', userDomain) ..add('userId', userId) @@ -79,40 +82,41 @@ class _$CalendarFilter extends CalendarFilter { class CalendarFilterBuilder implements Builder { - _$CalendarFilter _$v; + _$CalendarFilter? _$v; - int _id; - int get id => _$this._id; - set id(int id) => _$this._id = id; + int? _id; + int? get id => _$this._id; + set id(int? id) => _$this._id = id; - String _userDomain; - String get userDomain => _$this._userDomain; - set userDomain(String userDomain) => _$this._userDomain = userDomain; + String? _userDomain; + String? get userDomain => _$this._userDomain; + set userDomain(String? userDomain) => _$this._userDomain = userDomain; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _observeeId; - String get observeeId => _$this._observeeId; - set observeeId(String observeeId) => _$this._observeeId = observeeId; + String? _observeeId; + String? get observeeId => _$this._observeeId; + set observeeId(String? observeeId) => _$this._observeeId = observeeId; - SetBuilder _filters; + SetBuilder? _filters; SetBuilder get filters => _$this._filters ??= new SetBuilder(); - set filters(SetBuilder filters) => _$this._filters = filters; + set filters(SetBuilder? filters) => _$this._filters = filters; CalendarFilterBuilder() { CalendarFilter._initializeBuilder(this); } CalendarFilterBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _userDomain = _$v.userDomain; - _userId = _$v.userId; - _observeeId = _$v.observeeId; - _filters = _$v.filters?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _userDomain = $v.userDomain; + _userId = $v.userId; + _observeeId = $v.observeeId; + _filters = $v.filters.toBuilder(); _$v = null; } return this; @@ -120,36 +124,39 @@ class CalendarFilterBuilder @override void replace(CalendarFilter other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CalendarFilter; } @override - void update(void Function(CalendarFilterBuilder) updates) { + void update(void Function(CalendarFilterBuilder)? updates) { if (updates != null) updates(this); } @override - _$CalendarFilter build() { + CalendarFilter build() => _build(); + + _$CalendarFilter _build() { _$CalendarFilter _$result; try { _$result = _$v ?? new _$CalendarFilter._( id: id, - userDomain: userDomain, - userId: userId, - observeeId: observeeId, + userDomain: BuiltValueNullFieldError.checkNotNull( + userDomain, r'CalendarFilter', 'userDomain'), + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'CalendarFilter', 'userId'), + observeeId: BuiltValueNullFieldError.checkNotNull( + observeeId, r'CalendarFilter', 'observeeId'), filters: filters.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'filters'; filters.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CalendarFilter', _$failedField, e.toString()); + r'CalendarFilter', _$failedField, e.toString()); } rethrow; } @@ -158,4 +165,4 @@ class CalendarFilterBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/canvas_page.dart b/apps/flutter_parent/lib/models/canvas_page.dart index 9fef8bdc6e..73f90a20cf 100644 --- a/apps/flutter_parent/lib/models/canvas_page.dart +++ b/apps/flutter_parent/lib/models/canvas_page.dart @@ -30,28 +30,22 @@ abstract class CanvasPage implements Built { @BuiltValueField(wireName: 'page_id') String get id; - @nullable - String get url; + String? get url; - @nullable - String get title; + String? get title; - @nullable @BuiltValueField(wireName: 'created_at') - DateTime get createdAt; + DateTime? get createdAt; - @nullable @BuiltValueField(wireName: 'updated_at') - DateTime get updatedAt; + DateTime? get updatedAt; @BuiltValueField(wireName: 'hide_from_students') bool get hideFromStudents; - @nullable - String get status; + String? get status; - @nullable - String get body; + String? get body; @BuiltValueField(wireName: 'front_page') bool get frontPage; @@ -62,13 +56,11 @@ abstract class CanvasPage implements Built { @BuiltValueField(wireName: 'published') bool get published; - @nullable @BuiltValueField(wireName: 'editing_roles') - String get editingRoles; + String? get editingRoles; - @nullable @BuiltValueField(wireName: 'lock_explanation') - String get lockExplanation; + String? get lockExplanation; static void _initializeBuilder(CanvasPageBuilder b) => b ..hideFromStudents = false diff --git a/apps/flutter_parent/lib/models/canvas_page.g.dart b/apps/flutter_parent/lib/models/canvas_page.g.dart index 5af0fd440a..5b0413742e 100644 --- a/apps/flutter_parent/lib/models/canvas_page.g.dart +++ b/apps/flutter_parent/lib/models/canvas_page.g.dart @@ -15,9 +15,9 @@ class _$CanvasPageSerializer implements StructuredSerializer { final String wireName = 'CanvasPage'; @override - Iterable serialize(Serializers serializers, CanvasPage object, + Iterable serialize(Serializers serializers, CanvasPage object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'page_id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'hide_from_students', @@ -33,128 +33,121 @@ class _$CanvasPageSerializer implements StructuredSerializer { serializers.serialize(object.published, specifiedType: const FullType(bool)), ]; - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('title'); - if (object.title == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.title, - specifiedType: const FullType(String))); - } - result.add('created_at'); - if (object.createdAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.createdAt, + Object? value; + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.title; + + result + ..add('title') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.createdAt; + + result + ..add('created_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('updated_at'); - if (object.updatedAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.updatedAt, + value = object.updatedAt; + + result + ..add('updated_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('status'); - if (object.status == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.status, - specifiedType: const FullType(String))); - } - result.add('body'); - if (object.body == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.body, - specifiedType: const FullType(String))); - } - result.add('editing_roles'); - if (object.editingRoles == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.editingRoles, - specifiedType: const FullType(String))); - } - result.add('lock_explanation'); - if (object.lockExplanation == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockExplanation, - specifiedType: const FullType(String))); - } + value = object.status; + + result + ..add('status') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.body; + + result + ..add('body') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.editingRoles; + + result + ..add('editing_roles') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.lockExplanation; + + result + ..add('lock_explanation') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - CanvasPage deserialize(Serializers serializers, Iterable serialized, + CanvasPage deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CanvasPageBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'page_id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'created_at': result.createdAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'updated_at': result.updatedAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'hide_from_students': result.hideFromStudents = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'status': result.status = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'body': result.body = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'front_page': result.frontPage = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'lock_info': result.lockInfo.replace(serializers.deserialize(value, - specifiedType: const FullType(LockInfo)) as LockInfo); + specifiedType: const FullType(LockInfo))! as LockInfo); break; case 'published': result.published = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'editing_roles': result.editingRoles = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'lock_explanation': result.lockExplanation = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -167,19 +160,19 @@ class _$CanvasPage extends CanvasPage { @override final String id; @override - final String url; + final String? url; @override - final String title; + final String? title; @override - final DateTime createdAt; + final DateTime? createdAt; @override - final DateTime updatedAt; + final DateTime? updatedAt; @override final bool hideFromStudents; @override - final String status; + final String? status; @override - final String body; + final String? body; @override final bool frontPage; @override @@ -187,43 +180,36 @@ class _$CanvasPage extends CanvasPage { @override final bool published; @override - final String editingRoles; + final String? editingRoles; @override - final String lockExplanation; + final String? lockExplanation; - factory _$CanvasPage([void Function(CanvasPageBuilder) updates]) => - (new CanvasPageBuilder()..update(updates)).build(); + factory _$CanvasPage([void Function(CanvasPageBuilder)? updates]) => + (new CanvasPageBuilder()..update(updates))._build(); _$CanvasPage._( - {this.id, + {required this.id, this.url, this.title, this.createdAt, this.updatedAt, - this.hideFromStudents, + required this.hideFromStudents, this.status, this.body, - this.frontPage, - this.lockInfo, - this.published, + required this.frontPage, + required this.lockInfo, + required this.published, this.editingRoles, this.lockExplanation}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('CanvasPage', 'id'); - } - if (hideFromStudents == null) { - throw new BuiltValueNullFieldError('CanvasPage', 'hideFromStudents'); - } - if (frontPage == null) { - throw new BuiltValueNullFieldError('CanvasPage', 'frontPage'); - } - if (lockInfo == null) { - throw new BuiltValueNullFieldError('CanvasPage', 'lockInfo'); - } - if (published == null) { - throw new BuiltValueNullFieldError('CanvasPage', 'published'); - } + BuiltValueNullFieldError.checkNotNull(id, r'CanvasPage', 'id'); + BuiltValueNullFieldError.checkNotNull( + hideFromStudents, r'CanvasPage', 'hideFromStudents'); + BuiltValueNullFieldError.checkNotNull( + frontPage, r'CanvasPage', 'frontPage'); + BuiltValueNullFieldError.checkNotNull(lockInfo, r'CanvasPage', 'lockInfo'); + BuiltValueNullFieldError.checkNotNull( + published, r'CanvasPage', 'published'); } @override @@ -254,35 +240,27 @@ class _$CanvasPage extends CanvasPage { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc(0, id.hashCode), - url.hashCode), - title.hashCode), - createdAt.hashCode), - updatedAt.hashCode), - hideFromStudents.hashCode), - status.hashCode), - body.hashCode), - frontPage.hashCode), - lockInfo.hashCode), - published.hashCode), - editingRoles.hashCode), - lockExplanation.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, createdAt.hashCode); + _$hash = $jc(_$hash, updatedAt.hashCode); + _$hash = $jc(_$hash, hideFromStudents.hashCode); + _$hash = $jc(_$hash, status.hashCode); + _$hash = $jc(_$hash, body.hashCode); + _$hash = $jc(_$hash, frontPage.hashCode); + _$hash = $jc(_$hash, lockInfo.hashCode); + _$hash = $jc(_$hash, published.hashCode); + _$hash = $jc(_$hash, editingRoles.hashCode); + _$hash = $jc(_$hash, lockExplanation.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CanvasPage') + return (newBuiltValueToStringHelper(r'CanvasPage') ..add('id', id) ..add('url', url) ..add('title', title) @@ -301,60 +279,60 @@ class _$CanvasPage extends CanvasPage { } class CanvasPageBuilder implements Builder { - _$CanvasPage _$v; + _$CanvasPage? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - DateTime _createdAt; - DateTime get createdAt => _$this._createdAt; - set createdAt(DateTime createdAt) => _$this._createdAt = createdAt; + DateTime? _createdAt; + DateTime? get createdAt => _$this._createdAt; + set createdAt(DateTime? createdAt) => _$this._createdAt = createdAt; - DateTime _updatedAt; - DateTime get updatedAt => _$this._updatedAt; - set updatedAt(DateTime updatedAt) => _$this._updatedAt = updatedAt; + DateTime? _updatedAt; + DateTime? get updatedAt => _$this._updatedAt; + set updatedAt(DateTime? updatedAt) => _$this._updatedAt = updatedAt; - bool _hideFromStudents; - bool get hideFromStudents => _$this._hideFromStudents; - set hideFromStudents(bool hideFromStudents) => + bool? _hideFromStudents; + bool? get hideFromStudents => _$this._hideFromStudents; + set hideFromStudents(bool? hideFromStudents) => _$this._hideFromStudents = hideFromStudents; - String _status; - String get status => _$this._status; - set status(String status) => _$this._status = status; + String? _status; + String? get status => _$this._status; + set status(String? status) => _$this._status = status; - String _body; - String get body => _$this._body; - set body(String body) => _$this._body = body; + String? _body; + String? get body => _$this._body; + set body(String? body) => _$this._body = body; - bool _frontPage; - bool get frontPage => _$this._frontPage; - set frontPage(bool frontPage) => _$this._frontPage = frontPage; + bool? _frontPage; + bool? get frontPage => _$this._frontPage; + set frontPage(bool? frontPage) => _$this._frontPage = frontPage; - LockInfoBuilder _lockInfo; + LockInfoBuilder? _lockInfo; LockInfoBuilder get lockInfo => _$this._lockInfo ??= new LockInfoBuilder(); - set lockInfo(LockInfoBuilder lockInfo) => _$this._lockInfo = lockInfo; + set lockInfo(LockInfoBuilder? lockInfo) => _$this._lockInfo = lockInfo; - bool _published; - bool get published => _$this._published; - set published(bool published) => _$this._published = published; + bool? _published; + bool? get published => _$this._published; + set published(bool? published) => _$this._published = published; - String _editingRoles; - String get editingRoles => _$this._editingRoles; - set editingRoles(String editingRoles) => _$this._editingRoles = editingRoles; + String? _editingRoles; + String? get editingRoles => _$this._editingRoles; + set editingRoles(String? editingRoles) => _$this._editingRoles = editingRoles; - String _lockExplanation; - String get lockExplanation => _$this._lockExplanation; - set lockExplanation(String lockExplanation) => + String? _lockExplanation; + String? get lockExplanation => _$this._lockExplanation; + set lockExplanation(String? lockExplanation) => _$this._lockExplanation = lockExplanation; CanvasPageBuilder() { @@ -362,20 +340,21 @@ class CanvasPageBuilder implements Builder { } CanvasPageBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _url = _$v.url; - _title = _$v.title; - _createdAt = _$v.createdAt; - _updatedAt = _$v.updatedAt; - _hideFromStudents = _$v.hideFromStudents; - _status = _$v.status; - _body = _$v.body; - _frontPage = _$v.frontPage; - _lockInfo = _$v.lockInfo?.toBuilder(); - _published = _$v.published; - _editingRoles = _$v.editingRoles; - _lockExplanation = _$v.lockExplanation; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _url = $v.url; + _title = $v.title; + _createdAt = $v.createdAt; + _updatedAt = $v.updatedAt; + _hideFromStudents = $v.hideFromStudents; + _status = $v.status; + _body = $v.body; + _frontPage = $v.frontPage; + _lockInfo = $v.lockInfo.toBuilder(); + _published = $v.published; + _editingRoles = $v.editingRoles; + _lockExplanation = $v.lockExplanation; _$v = null; } return this; @@ -383,44 +362,48 @@ class CanvasPageBuilder implements Builder { @override void replace(CanvasPage other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CanvasPage; } @override - void update(void Function(CanvasPageBuilder) updates) { + void update(void Function(CanvasPageBuilder)? updates) { if (updates != null) updates(this); } @override - _$CanvasPage build() { + CanvasPage build() => _build(); + + _$CanvasPage _build() { _$CanvasPage _$result; try { _$result = _$v ?? new _$CanvasPage._( - id: id, + id: BuiltValueNullFieldError.checkNotNull( + id, r'CanvasPage', 'id'), url: url, title: title, createdAt: createdAt, updatedAt: updatedAt, - hideFromStudents: hideFromStudents, + hideFromStudents: BuiltValueNullFieldError.checkNotNull( + hideFromStudents, r'CanvasPage', 'hideFromStudents'), status: status, body: body, - frontPage: frontPage, + frontPage: BuiltValueNullFieldError.checkNotNull( + frontPage, r'CanvasPage', 'frontPage'), lockInfo: lockInfo.build(), - published: published, + published: BuiltValueNullFieldError.checkNotNull( + published, r'CanvasPage', 'published'), editingRoles: editingRoles, lockExplanation: lockExplanation); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'lockInfo'; lockInfo.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CanvasPage', _$failedField, e.toString()); + r'CanvasPage', _$failedField, e.toString()); } rethrow; } @@ -429,4 +412,4 @@ class CanvasPageBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/canvas_token.dart b/apps/flutter_parent/lib/models/canvas_token.dart index 15c98cacd7..571e79b168 100644 --- a/apps/flutter_parent/lib/models/canvas_token.dart +++ b/apps/flutter_parent/lib/models/canvas_token.dart @@ -29,14 +29,11 @@ abstract class CanvasToken implements Built { @BuiltValueField(wireName: 'access_token') String get accessToken; - @nullable @BuiltValueField(wireName: 'refresh_token') - String get refreshToken; + String? get refreshToken; - @nullable - User get user; + User? get user; - @nullable @BuiltValueField(wireName: 'real_user') - User get realUser; + User? get realUser; } diff --git a/apps/flutter_parent/lib/models/canvas_token.g.dart b/apps/flutter_parent/lib/models/canvas_token.g.dart index f7ff820b14..9166a153b9 100644 --- a/apps/flutter_parent/lib/models/canvas_token.g.dart +++ b/apps/flutter_parent/lib/models/canvas_token.g.dart @@ -15,64 +15,60 @@ class _$CanvasTokenSerializer implements StructuredSerializer { final String wireName = 'CanvasToken'; @override - Iterable serialize(Serializers serializers, CanvasToken object, + Iterable serialize(Serializers serializers, CanvasToken object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'access_token', serializers.serialize(object.accessToken, specifiedType: const FullType(String)), ]; - result.add('refresh_token'); - if (object.refreshToken == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.refreshToken, - specifiedType: const FullType(String))); - } - result.add('user'); - if (object.user == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.user, - specifiedType: const FullType(User))); - } - result.add('real_user'); - if (object.realUser == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.realUser, - specifiedType: const FullType(User))); - } + Object? value; + value = object.refreshToken; + + result + ..add('refresh_token') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.user; + + result + ..add('user') + ..add(serializers.serialize(value, specifiedType: const FullType(User))); + value = object.realUser; + + result + ..add('real_user') + ..add(serializers.serialize(value, specifiedType: const FullType(User))); + return result; } @override - CanvasToken deserialize(Serializers serializers, Iterable serialized, + CanvasToken deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CanvasTokenBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'access_token': result.accessToken = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'refresh_token': result.refreshToken = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'user': result.user.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; case 'real_user': result.realUser.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; } } @@ -85,21 +81,20 @@ class _$CanvasToken extends CanvasToken { @override final String accessToken; @override - final String refreshToken; + final String? refreshToken; @override - final User user; + final User? user; @override - final User realUser; + final User? realUser; - factory _$CanvasToken([void Function(CanvasTokenBuilder) updates]) => - (new CanvasTokenBuilder()..update(updates)).build(); + factory _$CanvasToken([void Function(CanvasTokenBuilder)? updates]) => + (new CanvasTokenBuilder()..update(updates))._build(); _$CanvasToken._( - {this.accessToken, this.refreshToken, this.user, this.realUser}) + {required this.accessToken, this.refreshToken, this.user, this.realUser}) : super._() { - if (accessToken == null) { - throw new BuiltValueNullFieldError('CanvasToken', 'accessToken'); - } + BuiltValueNullFieldError.checkNotNull( + accessToken, r'CanvasToken', 'accessToken'); } @override @@ -121,15 +116,18 @@ class _$CanvasToken extends CanvasToken { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, accessToken.hashCode), refreshToken.hashCode), - user.hashCode), - realUser.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, accessToken.hashCode); + _$hash = $jc(_$hash, refreshToken.hashCode); + _$hash = $jc(_$hash, user.hashCode); + _$hash = $jc(_$hash, realUser.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CanvasToken') + return (newBuiltValueToStringHelper(r'CanvasToken') ..add('accessToken', accessToken) ..add('refreshToken', refreshToken) ..add('user', user) @@ -139,32 +137,33 @@ class _$CanvasToken extends CanvasToken { } class CanvasTokenBuilder implements Builder { - _$CanvasToken _$v; + _$CanvasToken? _$v; - String _accessToken; - String get accessToken => _$this._accessToken; - set accessToken(String accessToken) => _$this._accessToken = accessToken; + String? _accessToken; + String? get accessToken => _$this._accessToken; + set accessToken(String? accessToken) => _$this._accessToken = accessToken; - String _refreshToken; - String get refreshToken => _$this._refreshToken; - set refreshToken(String refreshToken) => _$this._refreshToken = refreshToken; + String? _refreshToken; + String? get refreshToken => _$this._refreshToken; + set refreshToken(String? refreshToken) => _$this._refreshToken = refreshToken; - UserBuilder _user; + UserBuilder? _user; UserBuilder get user => _$this._user ??= new UserBuilder(); - set user(UserBuilder user) => _$this._user = user; + set user(UserBuilder? user) => _$this._user = user; - UserBuilder _realUser; + UserBuilder? _realUser; UserBuilder get realUser => _$this._realUser ??= new UserBuilder(); - set realUser(UserBuilder realUser) => _$this._realUser = realUser; + set realUser(UserBuilder? realUser) => _$this._realUser = realUser; CanvasTokenBuilder(); CanvasTokenBuilder get _$this { - if (_$v != null) { - _accessToken = _$v.accessToken; - _refreshToken = _$v.refreshToken; - _user = _$v.user?.toBuilder(); - _realUser = _$v.realUser?.toBuilder(); + final $v = _$v; + if ($v != null) { + _accessToken = $v.accessToken; + _refreshToken = $v.refreshToken; + _user = $v.user?.toBuilder(); + _realUser = $v.realUser?.toBuilder(); _$v = null; } return this; @@ -172,29 +171,30 @@ class CanvasTokenBuilder implements Builder { @override void replace(CanvasToken other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CanvasToken; } @override - void update(void Function(CanvasTokenBuilder) updates) { + void update(void Function(CanvasTokenBuilder)? updates) { if (updates != null) updates(this); } @override - _$CanvasToken build() { + CanvasToken build() => _build(); + + _$CanvasToken _build() { _$CanvasToken _$result; try { _$result = _$v ?? new _$CanvasToken._( - accessToken: accessToken, + accessToken: BuiltValueNullFieldError.checkNotNull( + accessToken, r'CanvasToken', 'accessToken'), refreshToken: refreshToken, user: _user?.build(), realUser: _realUser?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'user'; _user?.build(); @@ -202,7 +202,7 @@ class CanvasTokenBuilder implements Builder { _realUser?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CanvasToken', _$failedField, e.toString()); + r'CanvasToken', _$failedField, e.toString()); } rethrow; } @@ -211,4 +211,4 @@ class CanvasTokenBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/color_change_response.dart b/apps/flutter_parent/lib/models/color_change_response.dart index 19f87b6e25..8837da56c7 100644 --- a/apps/flutter_parent/lib/models/color_change_response.dart +++ b/apps/flutter_parent/lib/models/color_change_response.dart @@ -10,8 +10,7 @@ abstract class ColorChangeResponse implements Built get serializer => _$colorChangeResponseSerializer; @BuiltValueField(wireName: 'hexcode') - @nullable - String get hexCode; + String? get hexCode; ColorChangeResponse._(); factory ColorChangeResponse([void Function(ColorChangeResponseBuilder) updates]) = _$ColorChangeResponse; diff --git a/apps/flutter_parent/lib/models/color_change_response.g.dart b/apps/flutter_parent/lib/models/color_change_response.g.dart index ccfc5d6d51..e9622d18a8 100644 --- a/apps/flutter_parent/lib/models/color_change_response.g.dart +++ b/apps/flutter_parent/lib/models/color_change_response.g.dart @@ -20,11 +20,11 @@ class _$ColorChangeResponseSerializer final String wireName = 'ColorChangeResponse'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, ColorChangeResponse object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - Object value; + final result = []; + Object? value; value = object.hexCode; result @@ -37,19 +37,19 @@ class _$ColorChangeResponseSerializer @override ColorChangeResponse deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new ColorChangeResponseBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'hexcode': result.hexCode = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -60,11 +60,11 @@ class _$ColorChangeResponseSerializer class _$ColorChangeResponse extends ColorChangeResponse { @override - final String hexCode; + final String? hexCode; factory _$ColorChangeResponse( - [void Function(ColorChangeResponseBuilder) updates]) => - (new ColorChangeResponseBuilder()..update(updates)).build(); + [void Function(ColorChangeResponseBuilder)? updates]) => + (new ColorChangeResponseBuilder()..update(updates))._build(); _$ColorChangeResponse._({this.hexCode}) : super._(); @@ -85,12 +85,15 @@ class _$ColorChangeResponse extends ColorChangeResponse { @override int get hashCode { - return $jf($jc(0, hexCode.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, hexCode.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('ColorChangeResponse') + return (newBuiltValueToStringHelper(r'ColorChangeResponse') ..add('hexCode', hexCode)) .toString(); } @@ -98,11 +101,11 @@ class _$ColorChangeResponse extends ColorChangeResponse { class ColorChangeResponseBuilder implements Builder { - _$ColorChangeResponse _$v; + _$ColorChangeResponse? _$v; - String _hexCode; - String get hexCode => _$this._hexCode; - set hexCode(String hexCode) => _$this._hexCode = hexCode; + String? _hexCode; + String? get hexCode => _$this._hexCode; + set hexCode(String? hexCode) => _$this._hexCode = hexCode; ColorChangeResponseBuilder() { ColorChangeResponse._initializeBuilder(this); @@ -124,16 +127,18 @@ class ColorChangeResponseBuilder } @override - void update(void Function(ColorChangeResponseBuilder) updates) { + void update(void Function(ColorChangeResponseBuilder)? updates) { if (updates != null) updates(this); } @override - _$ColorChangeResponse build() { + ColorChangeResponse build() => _build(); + + _$ColorChangeResponse _build() { final _$result = _$v ?? new _$ColorChangeResponse._(hexCode: hexCode); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/conversation.dart b/apps/flutter_parent/lib/models/conversation.dart index 4e5efda247..fbc6f2a6a5 100644 --- a/apps/flutter_parent/lib/models/conversation.dart +++ b/apps/flutter_parent/lib/models/conversation.dart @@ -39,22 +39,18 @@ abstract class Conversation implements Built /// 100 character preview of the last message @BuiltValueField(wireName: 'last_message') - @nullable - String get lastMessage; + String? get lastMessage; /// 100 character preview of the last authored message @BuiltValueField(wireName: 'last_authored_message') - @nullable - String get lastAuthoredMessage; + String? get lastAuthoredMessage; /// Date of the last message sent @BuiltValueField(wireName: 'last_message_at') - @nullable - DateTime get lastMessageAt; + DateTime? get lastMessageAt; @BuiltValueField(wireName: 'last_authored_message_at') - @nullable - DateTime get lastAuthoredMessageAt; + DateTime? get lastAuthoredMessageAt; // Number of messages in the conversation. @BuiltValueField(wireName: 'message_count') @@ -70,32 +66,26 @@ abstract class Conversation implements Built /// The avatar to display. Knows if group, user, etc. @BuiltValueField(wireName: 'avatar_url') - @nullable - String get avatarUrl; + String? get avatarUrl; /// Whether this conversation is visible in the current context. Not 100% what that means @BuiltValueField(wireName: 'visible') bool get isVisible; /// The IDs of all people in the conversation. EXCLUDING the current user unless it's a monologue - @nullable - BuiltList get audience; + BuiltList? get audience; /// The name and IDs of all participants in the conversation - @nullable - BuiltList get participants; + BuiltList? get participants; /// Messages attached to the conversation - @nullable - BuiltList get messages; + BuiltList? get messages; @BuiltValueField(wireName: 'context_name') - @nullable - String get contextName; + String? get contextName; @BuiltValueField(wireName: 'context_code') - @nullable - String get contextCode; + String? get contextCode; bool isUnread() => workflowState == ConversationWorkflowState.unread; @@ -111,9 +101,12 @@ abstract class Conversation implements Built ..isStarred = false ..isVisible = false; - String getContextId() { - final index = contextCode.indexOf('_'); - return contextCode.substring(index + 1, contextCode.length); + String? getContextId() { + if (contextCode == null) { + return null; + } + final index = contextCode!.indexOf('_'); + return contextCode!.substring(index + 1, contextCode!.length); } } diff --git a/apps/flutter_parent/lib/models/conversation.g.dart b/apps/flutter_parent/lib/models/conversation.g.dart index b9d6ae6e3a..e1e6beac97 100644 --- a/apps/flutter_parent/lib/models/conversation.g.dart +++ b/apps/flutter_parent/lib/models/conversation.g.dart @@ -50,9 +50,9 @@ class _$ConversationSerializer implements StructuredSerializer { final String wireName = 'Conversation'; @override - Iterable serialize(Serializers serializers, Conversation object, + Iterable serialize(Serializers serializers, Conversation object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'subject', @@ -74,157 +74,169 @@ class _$ConversationSerializer implements StructuredSerializer { serializers.serialize(object.isVisible, specifiedType: const FullType(bool)), ]; - if (object.lastMessage != null) { + Object? value; + value = object.lastMessage; + if (value != null) { result ..add('last_message') - ..add(serializers.serialize(object.lastMessage, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - if (object.lastAuthoredMessage != null) { + value = object.lastAuthoredMessage; + if (value != null) { result ..add('last_authored_message') - ..add(serializers.serialize(object.lastAuthoredMessage, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - if (object.lastMessageAt != null) { + value = object.lastMessageAt; + if (value != null) { result ..add('last_message_at') - ..add(serializers.serialize(object.lastMessageAt, + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); } - if (object.lastAuthoredMessageAt != null) { + value = object.lastAuthoredMessageAt; + if (value != null) { result ..add('last_authored_message_at') - ..add(serializers.serialize(object.lastAuthoredMessageAt, + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); } - if (object.avatarUrl != null) { + value = object.avatarUrl; + if (value != null) { result ..add('avatar_url') - ..add(serializers.serialize(object.avatarUrl, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - if (object.audience != null) { + value = object.audience; + if (value != null) { result ..add('audience') - ..add(serializers.serialize(object.audience, + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltList, const [const FullType(String)]))); } - if (object.participants != null) { + value = object.participants; + if (value != null) { result ..add('participants') - ..add(serializers.serialize(object.participants, + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltList, const [const FullType(BasicUser)]))); } - if (object.messages != null) { + value = object.messages; + if (value != null) { result ..add('messages') - ..add(serializers.serialize(object.messages, + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltList, const [const FullType(Message)]))); } - if (object.contextName != null) { + value = object.contextName; + if (value != null) { result ..add('context_name') - ..add(serializers.serialize(object.contextName, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - if (object.contextCode != null) { + value = object.contextCode; + if (value != null) { result ..add('context_code') - ..add(serializers.serialize(object.contextCode, + ..add(serializers.serialize(value, specifiedType: const FullType(String))); } return result; } @override - Conversation deserialize(Serializers serializers, Iterable serialized, + Conversation deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new ConversationBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'subject': result.subject = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'workflow_state': result.workflowState = serializers.deserialize(value, - specifiedType: const FullType(ConversationWorkflowState)) + specifiedType: const FullType(ConversationWorkflowState))! as ConversationWorkflowState; break; case 'last_message': result.lastMessage = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'last_authored_message': result.lastAuthoredMessage = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'last_message_at': result.lastMessageAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'last_authored_message_at': result.lastAuthoredMessageAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'message_count': result.messageCount = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; case 'subscribed': result.isSubscribed = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'starred': result.isStarred = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'avatar_url': result.avatarUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'visible': result.isVisible = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'audience': result.audience.replace(serializers.deserialize(value, - specifiedType: - const FullType(BuiltList, const [const FullType(String)])) - as BuiltList); + specifiedType: const FullType( + BuiltList, const [const FullType(String)]))! + as BuiltList); break; case 'participants': result.participants.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(BasicUser)])) - as BuiltList); + BuiltList, const [const FullType(BasicUser)]))! + as BuiltList); break; case 'messages': result.messages.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Message)])) - as BuiltList); + BuiltList, const [const FullType(Message)]))! + as BuiltList); break; case 'context_name': result.contextName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'context_code': result.contextCode = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -260,13 +272,13 @@ class _$Conversation extends Conversation { @override final ConversationWorkflowState workflowState; @override - final String lastMessage; + final String? lastMessage; @override - final String lastAuthoredMessage; + final String? lastAuthoredMessage; @override - final DateTime lastMessageAt; + final DateTime? lastMessageAt; @override - final DateTime lastAuthoredMessageAt; + final DateTime? lastAuthoredMessageAt; @override final int messageCount; @override @@ -274,63 +286,54 @@ class _$Conversation extends Conversation { @override final bool isStarred; @override - final String avatarUrl; + final String? avatarUrl; @override final bool isVisible; @override - final BuiltList audience; + final BuiltList? audience; @override - final BuiltList participants; + final BuiltList? participants; @override - final BuiltList messages; + final BuiltList? messages; @override - final String contextName; + final String? contextName; @override - final String contextCode; + final String? contextCode; - factory _$Conversation([void Function(ConversationBuilder) updates]) => - (new ConversationBuilder()..update(updates)).build(); + factory _$Conversation([void Function(ConversationBuilder)? updates]) => + (new ConversationBuilder()..update(updates))._build(); _$Conversation._( - {this.id, - this.subject, - this.workflowState, + {required this.id, + required this.subject, + required this.workflowState, this.lastMessage, this.lastAuthoredMessage, this.lastMessageAt, this.lastAuthoredMessageAt, - this.messageCount, - this.isSubscribed, - this.isStarred, + required this.messageCount, + required this.isSubscribed, + required this.isStarred, this.avatarUrl, - this.isVisible, + required this.isVisible, this.audience, this.participants, this.messages, this.contextName, this.contextCode}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Conversation', 'id'); - } - if (subject == null) { - throw new BuiltValueNullFieldError('Conversation', 'subject'); - } - if (workflowState == null) { - throw new BuiltValueNullFieldError('Conversation', 'workflowState'); - } - if (messageCount == null) { - throw new BuiltValueNullFieldError('Conversation', 'messageCount'); - } - if (isSubscribed == null) { - throw new BuiltValueNullFieldError('Conversation', 'isSubscribed'); - } - if (isStarred == null) { - throw new BuiltValueNullFieldError('Conversation', 'isStarred'); - } - if (isVisible == null) { - throw new BuiltValueNullFieldError('Conversation', 'isVisible'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Conversation', 'id'); + BuiltValueNullFieldError.checkNotNull(subject, r'Conversation', 'subject'); + BuiltValueNullFieldError.checkNotNull( + workflowState, r'Conversation', 'workflowState'); + BuiltValueNullFieldError.checkNotNull( + messageCount, r'Conversation', 'messageCount'); + BuiltValueNullFieldError.checkNotNull( + isSubscribed, r'Conversation', 'isSubscribed'); + BuiltValueNullFieldError.checkNotNull( + isStarred, r'Conversation', 'isStarred'); + BuiltValueNullFieldError.checkNotNull( + isVisible, r'Conversation', 'isVisible'); } @override @@ -365,51 +368,31 @@ class _$Conversation extends Conversation { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - 0, - id - .hashCode), - subject - .hashCode), - workflowState - .hashCode), - lastMessage - .hashCode), - lastAuthoredMessage - .hashCode), - lastMessageAt.hashCode), - lastAuthoredMessageAt.hashCode), - messageCount.hashCode), - isSubscribed.hashCode), - isStarred.hashCode), - avatarUrl.hashCode), - isVisible.hashCode), - audience.hashCode), - participants.hashCode), - messages.hashCode), - contextName.hashCode), - contextCode.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, subject.hashCode); + _$hash = $jc(_$hash, workflowState.hashCode); + _$hash = $jc(_$hash, lastMessage.hashCode); + _$hash = $jc(_$hash, lastAuthoredMessage.hashCode); + _$hash = $jc(_$hash, lastMessageAt.hashCode); + _$hash = $jc(_$hash, lastAuthoredMessageAt.hashCode); + _$hash = $jc(_$hash, messageCount.hashCode); + _$hash = $jc(_$hash, isSubscribed.hashCode); + _$hash = $jc(_$hash, isStarred.hashCode); + _$hash = $jc(_$hash, avatarUrl.hashCode); + _$hash = $jc(_$hash, isVisible.hashCode); + _$hash = $jc(_$hash, audience.hashCode); + _$hash = $jc(_$hash, participants.hashCode); + _$hash = $jc(_$hash, messages.hashCode); + _$hash = $jc(_$hash, contextName.hashCode); + _$hash = $jc(_$hash, contextCode.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Conversation') + return (newBuiltValueToStringHelper(r'Conversation') ..add('id', id) ..add('subject', subject) ..add('workflowState', workflowState) @@ -433,107 +416,108 @@ class _$Conversation extends Conversation { class ConversationBuilder implements Builder { - _$Conversation _$v; + _$Conversation? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _subject; - String get subject => _$this._subject; - set subject(String subject) => _$this._subject = subject; + String? _subject; + String? get subject => _$this._subject; + set subject(String? subject) => _$this._subject = subject; - ConversationWorkflowState _workflowState; - ConversationWorkflowState get workflowState => _$this._workflowState; - set workflowState(ConversationWorkflowState workflowState) => + ConversationWorkflowState? _workflowState; + ConversationWorkflowState? get workflowState => _$this._workflowState; + set workflowState(ConversationWorkflowState? workflowState) => _$this._workflowState = workflowState; - String _lastMessage; - String get lastMessage => _$this._lastMessage; - set lastMessage(String lastMessage) => _$this._lastMessage = lastMessage; + String? _lastMessage; + String? get lastMessage => _$this._lastMessage; + set lastMessage(String? lastMessage) => _$this._lastMessage = lastMessage; - String _lastAuthoredMessage; - String get lastAuthoredMessage => _$this._lastAuthoredMessage; - set lastAuthoredMessage(String lastAuthoredMessage) => + String? _lastAuthoredMessage; + String? get lastAuthoredMessage => _$this._lastAuthoredMessage; + set lastAuthoredMessage(String? lastAuthoredMessage) => _$this._lastAuthoredMessage = lastAuthoredMessage; - DateTime _lastMessageAt; - DateTime get lastMessageAt => _$this._lastMessageAt; - set lastMessageAt(DateTime lastMessageAt) => + DateTime? _lastMessageAt; + DateTime? get lastMessageAt => _$this._lastMessageAt; + set lastMessageAt(DateTime? lastMessageAt) => _$this._lastMessageAt = lastMessageAt; - DateTime _lastAuthoredMessageAt; - DateTime get lastAuthoredMessageAt => _$this._lastAuthoredMessageAt; - set lastAuthoredMessageAt(DateTime lastAuthoredMessageAt) => + DateTime? _lastAuthoredMessageAt; + DateTime? get lastAuthoredMessageAt => _$this._lastAuthoredMessageAt; + set lastAuthoredMessageAt(DateTime? lastAuthoredMessageAt) => _$this._lastAuthoredMessageAt = lastAuthoredMessageAt; - int _messageCount; - int get messageCount => _$this._messageCount; - set messageCount(int messageCount) => _$this._messageCount = messageCount; + int? _messageCount; + int? get messageCount => _$this._messageCount; + set messageCount(int? messageCount) => _$this._messageCount = messageCount; - bool _isSubscribed; - bool get isSubscribed => _$this._isSubscribed; - set isSubscribed(bool isSubscribed) => _$this._isSubscribed = isSubscribed; + bool? _isSubscribed; + bool? get isSubscribed => _$this._isSubscribed; + set isSubscribed(bool? isSubscribed) => _$this._isSubscribed = isSubscribed; - bool _isStarred; - bool get isStarred => _$this._isStarred; - set isStarred(bool isStarred) => _$this._isStarred = isStarred; + bool? _isStarred; + bool? get isStarred => _$this._isStarred; + set isStarred(bool? isStarred) => _$this._isStarred = isStarred; - String _avatarUrl; - String get avatarUrl => _$this._avatarUrl; - set avatarUrl(String avatarUrl) => _$this._avatarUrl = avatarUrl; + String? _avatarUrl; + String? get avatarUrl => _$this._avatarUrl; + set avatarUrl(String? avatarUrl) => _$this._avatarUrl = avatarUrl; - bool _isVisible; - bool get isVisible => _$this._isVisible; - set isVisible(bool isVisible) => _$this._isVisible = isVisible; + bool? _isVisible; + bool? get isVisible => _$this._isVisible; + set isVisible(bool? isVisible) => _$this._isVisible = isVisible; - ListBuilder _audience; + ListBuilder? _audience; ListBuilder get audience => _$this._audience ??= new ListBuilder(); - set audience(ListBuilder audience) => _$this._audience = audience; + set audience(ListBuilder? audience) => _$this._audience = audience; - ListBuilder _participants; + ListBuilder? _participants; ListBuilder get participants => _$this._participants ??= new ListBuilder(); - set participants(ListBuilder participants) => + set participants(ListBuilder? participants) => _$this._participants = participants; - ListBuilder _messages; + ListBuilder? _messages; ListBuilder get messages => _$this._messages ??= new ListBuilder(); - set messages(ListBuilder messages) => _$this._messages = messages; + set messages(ListBuilder? messages) => _$this._messages = messages; - String _contextName; - String get contextName => _$this._contextName; - set contextName(String contextName) => _$this._contextName = contextName; + String? _contextName; + String? get contextName => _$this._contextName; + set contextName(String? contextName) => _$this._contextName = contextName; - String _contextCode; - String get contextCode => _$this._contextCode; - set contextCode(String contextCode) => _$this._contextCode = contextCode; + String? _contextCode; + String? get contextCode => _$this._contextCode; + set contextCode(String? contextCode) => _$this._contextCode = contextCode; ConversationBuilder() { Conversation._initializeBuilder(this); } ConversationBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _subject = _$v.subject; - _workflowState = _$v.workflowState; - _lastMessage = _$v.lastMessage; - _lastAuthoredMessage = _$v.lastAuthoredMessage; - _lastMessageAt = _$v.lastMessageAt; - _lastAuthoredMessageAt = _$v.lastAuthoredMessageAt; - _messageCount = _$v.messageCount; - _isSubscribed = _$v.isSubscribed; - _isStarred = _$v.isStarred; - _avatarUrl = _$v.avatarUrl; - _isVisible = _$v.isVisible; - _audience = _$v.audience?.toBuilder(); - _participants = _$v.participants?.toBuilder(); - _messages = _$v.messages?.toBuilder(); - _contextName = _$v.contextName; - _contextCode = _$v.contextCode; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _subject = $v.subject; + _workflowState = $v.workflowState; + _lastMessage = $v.lastMessage; + _lastAuthoredMessage = $v.lastAuthoredMessage; + _lastMessageAt = $v.lastMessageAt; + _lastAuthoredMessageAt = $v.lastAuthoredMessageAt; + _messageCount = $v.messageCount; + _isSubscribed = $v.isSubscribed; + _isStarred = $v.isStarred; + _avatarUrl = $v.avatarUrl; + _isVisible = $v.isVisible; + _audience = $v.audience?.toBuilder(); + _participants = $v.participants?.toBuilder(); + _messages = $v.messages?.toBuilder(); + _contextName = $v.contextName; + _contextCode = $v.contextCode; _$v = null; } return this; @@ -541,42 +525,49 @@ class ConversationBuilder @override void replace(Conversation other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Conversation; } @override - void update(void Function(ConversationBuilder) updates) { + void update(void Function(ConversationBuilder)? updates) { if (updates != null) updates(this); } @override - _$Conversation build() { + Conversation build() => _build(); + + _$Conversation _build() { _$Conversation _$result; try { _$result = _$v ?? new _$Conversation._( - id: id, - subject: subject, - workflowState: workflowState, + id: BuiltValueNullFieldError.checkNotNull( + id, r'Conversation', 'id'), + subject: BuiltValueNullFieldError.checkNotNull( + subject, r'Conversation', 'subject'), + workflowState: BuiltValueNullFieldError.checkNotNull( + workflowState, r'Conversation', 'workflowState'), lastMessage: lastMessage, lastAuthoredMessage: lastAuthoredMessage, lastMessageAt: lastMessageAt, lastAuthoredMessageAt: lastAuthoredMessageAt, - messageCount: messageCount, - isSubscribed: isSubscribed, - isStarred: isStarred, + messageCount: BuiltValueNullFieldError.checkNotNull( + messageCount, r'Conversation', 'messageCount'), + isSubscribed: BuiltValueNullFieldError.checkNotNull( + isSubscribed, r'Conversation', 'isSubscribed'), + isStarred: BuiltValueNullFieldError.checkNotNull( + isStarred, r'Conversation', 'isStarred'), avatarUrl: avatarUrl, - isVisible: isVisible, + isVisible: BuiltValueNullFieldError.checkNotNull( + isVisible, r'Conversation', 'isVisible'), audience: _audience?.build(), participants: _participants?.build(), messages: _messages?.build(), contextName: contextName, contextCode: contextCode); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'audience'; _audience?.build(); @@ -586,7 +577,7 @@ class ConversationBuilder _messages?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Conversation', _$failedField, e.toString()); + r'Conversation', _$failedField, e.toString()); } rethrow; } @@ -595,4 +586,4 @@ class ConversationBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/course.dart b/apps/flutter_parent/lib/models/course.dart index 931a1bc5b0..9c704943da 100644 --- a/apps/flutter_parent/lib/models/course.dart +++ b/apps/flutter_parent/lib/models/course.dart @@ -17,6 +17,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/json_object.dart'; import 'package:built_value/serializer.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_parent/models/course_settings.dart'; import 'package:flutter_parent/models/grading_scheme_item.dart'; import 'package:flutter_parent/models/section.dart'; @@ -38,45 +39,36 @@ abstract class Course implements Built { factory Course([void Function(CourseBuilder) updates]) = _$Course; // Helper variables - @nullable @BuiltValueField(serialize: false) - double get currentScore; + double? get currentScore; - @nullable @BuiltValueField(serialize: false) - double get finalScore; + double? get finalScore; - @nullable @BuiltValueField(serialize: false) - String get currentGrade; + String? get currentGrade; - @nullable @BuiltValueField(serialize: false) - String get finalGrade; + String? get finalGrade; String get id; String get name; - @nullable @BuiltValueField(wireName: 'original_name') - String get originalName; + String? get originalName; - @nullable @BuiltValueField(wireName: 'course_code') - String get courseCode; + String? get courseCode; - @nullable @BuiltValueField(wireName: 'start_at') - DateTime get startAt; + DateTime? get startAt; - @nullable @BuiltValueField(wireName: 'end_at') - DateTime get endAt; + DateTime? get endAt; - @nullable @BuiltValueField(wireName: 'syllabus_body') - String get syllabusBody; + String? get syllabusBody; @BuiltValueField(wireName: 'hide_final_grades') bool get hideFinalGrades; @@ -84,7 +76,7 @@ abstract class Course implements Built { @BuiltValueField(wireName: 'is_public') bool get isPublic; - BuiltList get enrollments; + BuiltList? get enrollments; @BuiltValueField(wireName: 'needs_grading_count') int get needsGradingCount; @@ -98,10 +90,8 @@ abstract class Course implements Built { @BuiltValueField(wireName: 'access_restricted_by_date') bool get accessRestrictedByDate; - @nullable @BuiltValueField(wireName: 'image_download_url') - @nullable - String get imageDownloadUrl; + String? get imageDownloadUrl; @BuiltValueField(wireName: 'has_weighted_grading_periods') bool get hasWeightedGradingPeriods; @@ -112,31 +102,25 @@ abstract class Course implements Built { @BuiltValueField(wireName: 'restrict_enrollments_to_course_dates') bool get restrictEnrollmentsToCourseDates; - @nullable @BuiltValueField(wireName: 'workflow_state') - String get workflowState; + String? get workflowState; - @nullable @BuiltValueField(wireName: 'default_view') - HomePage get homePage; + HomePage? get homePage; - @nullable - Term get term; + Term? get term; - @nullable - BuiltList
get sections; + BuiltList
? get sections; - @nullable - CourseSettings get settings; + CourseSettings? get settings; - @nullable @BuiltValueField(wireName: 'grading_scheme') - BuiltList get gradingScheme; + BuiltList? get gradingScheme; List get gradingSchemeItems { if (gradingScheme == null) return []; - return gradingScheme.map((item) => GradingSchemeItem.fromJson(item)).where((element) => element != null).toList() - ..sort((a, b) => b.value.compareTo(a.value)); + return gradingScheme!.map((item) => GradingSchemeItem.fromJson(item)).nonNulls.where((element) => element.grade != null && element.value != null).toList() + ..sort((a, b) => b.value!.compareTo(a.value!)); } static void _initializeBuilder(CourseBuilder b) => b @@ -163,21 +147,20 @@ abstract class Course implements Built { /// [gradingPeriodId] -> Only used when [enrollment] is not provided or is null, when pulling the student's enrollment /// from the course will also match based on [Enrollment.currentGradingPeriodId] CourseGrade getCourseGrade( - String studentId, { - Enrollment enrollment, - String gradingPeriodId, + String? studentId, { + Enrollment? enrollment, + String? gradingPeriodId, bool forceAllPeriods = false, }) => CourseGrade( this, enrollment ?? - enrollments.firstWhere( + enrollments?.firstWhereOrNull( (enrollment) => enrollment.userId == studentId && (gradingPeriodId == null || gradingPeriodId.isEmpty || gradingPeriodId == enrollment.currentGradingPeriodId), - orElse: () => null, ), forceAllPeriods: forceAllPeriods, ); @@ -185,14 +168,14 @@ abstract class Course implements Built { String contextFilterId() => 'course_${this.id}'; /// Filters enrollments by those associated with the currently selected user - bool isValidForCurrentStudent(String currentStudentId) { + bool isValidForCurrentStudent(String? currentStudentId) { return enrollments?.any((enrollment) => enrollment.userId == currentStudentId) ?? false; } String convertScoreToLetterGrade(double score, double maxScore) { if (maxScore == 0.0 || gradingSchemeItems.isEmpty) return ""; double percent = score / maxScore; - return gradingSchemeItems.firstWhere((element) => percent >= element.value, orElse: () => gradingSchemeItems.last).grade; + return gradingSchemeItems.firstWhere((element) => percent >= element.value!, orElse: () => gradingSchemeItems.last).grade!; } } diff --git a/apps/flutter_parent/lib/models/course.g.dart b/apps/flutter_parent/lib/models/course.g.dart index 737bb7026e..4da07d1242 100644 --- a/apps/flutter_parent/lib/models/course.g.dart +++ b/apps/flutter_parent/lib/models/course.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of course; +part of 'course.dart'; // ************************************************************************** // BuiltValueGenerator @@ -48,9 +48,9 @@ class _$CourseSerializer implements StructuredSerializer { final String wireName = 'Course'; @override - Iterable serialize(Serializers serializers, Course object, + Iterable serialize(Serializers serializers, Course object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', @@ -61,10 +61,6 @@ class _$CourseSerializer implements StructuredSerializer { 'is_public', serializers.serialize(object.isPublic, specifiedType: const FullType(bool)), - 'enrollments', - serializers.serialize(object.enrollments, - specifiedType: - const FullType(BuiltList, const [const FullType(Enrollment)])), 'needs_grading_count', serializers.serialize(object.needsGradingCount, specifiedType: const FullType(int)), @@ -87,7 +83,7 @@ class _$CourseSerializer implements StructuredSerializer { serializers.serialize(object.restrictEnrollmentsToCourseDates, specifiedType: const FullType(bool)), ]; - Object value; + Object? value; value = object.originalName; result @@ -118,6 +114,13 @@ class _$CourseSerializer implements StructuredSerializer { ..add('syllabus_body') ..add( serializers.serialize(value, specifiedType: const FullType(String))); + value = object.enrollments; + + result + ..add('enrollments') + ..add(serializers.serialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(Enrollment)]))); value = object.imageDownloadUrl; result @@ -166,117 +169,118 @@ class _$CourseSerializer implements StructuredSerializer { } @override - Course deserialize(Serializers serializers, Iterable serialized, + Course deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CourseBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'original_name': result.originalName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'course_code': result.courseCode = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'start_at': result.startAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'end_at': result.endAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'syllabus_body': result.syllabusBody = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'hide_final_grades': result.hideFinalGrades = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'is_public': result.isPublic = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'enrollments': result.enrollments.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Enrollment)])) - as BuiltList); + BuiltList, const [const FullType(Enrollment)]))! + as BuiltList); break; case 'needs_grading_count': result.needsGradingCount = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; case 'apply_assignment_group_weights': result.applyAssignmentGroupWeights = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'is_favorite': result.isFavorite = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'access_restricted_by_date': result.accessRestrictedByDate = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'image_download_url': result.imageDownloadUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'has_weighted_grading_periods': result.hasWeightedGradingPeriods = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'has_grading_periods': result.hasGradingPeriods = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'restrict_enrollments_to_course_dates': result.restrictEnrollmentsToCourseDates = serializers - .deserialize(value, specifiedType: const FullType(bool)) as bool; + .deserialize(value, specifiedType: const FullType(bool))! as bool; break; case 'workflow_state': result.workflowState = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'default_view': result.homePage = serializers.deserialize(value, - specifiedType: const FullType(HomePage)) as HomePage; + specifiedType: const FullType(HomePage)) as HomePage?; break; case 'term': result.term.replace(serializers.deserialize(value, - specifiedType: const FullType(Term)) as Term); + specifiedType: const FullType(Term))! as Term); break; case 'sections': result.sections.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Section)])) - as BuiltList); + BuiltList, const [const FullType(Section)]))! + as BuiltList); break; case 'settings': result.settings.replace(serializers.deserialize(value, - specifiedType: const FullType(CourseSettings)) as CourseSettings); + specifiedType: const FullType(CourseSettings))! + as CourseSettings); break; case 'grading_scheme': result.gradingScheme.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(JsonObject)])) - as BuiltList); + BuiltList, const [const FullType(JsonObject)]))! + as BuiltList); break; } } @@ -304,33 +308,33 @@ class _$HomePageSerializer implements PrimitiveSerializer { class _$Course extends Course { @override - final double currentScore; + final double? currentScore; @override - final double finalScore; + final double? finalScore; @override - final String currentGrade; + final String? currentGrade; @override - final String finalGrade; + final String? finalGrade; @override final String id; @override final String name; @override - final String originalName; + final String? originalName; @override - final String courseCode; + final String? courseCode; @override - final DateTime startAt; + final DateTime? startAt; @override - final DateTime endAt; + final DateTime? endAt; @override - final String syllabusBody; + final String? syllabusBody; @override final bool hideFinalGrades; @override final bool isPublic; @override - final BuiltList enrollments; + final BuiltList? enrollments; @override final int needsGradingCount; @override @@ -340,7 +344,7 @@ class _$Course extends Course { @override final bool accessRestrictedByDate; @override - final String imageDownloadUrl; + final String? imageDownloadUrl; @override final bool hasWeightedGradingPeriods; @override @@ -348,44 +352,44 @@ class _$Course extends Course { @override final bool restrictEnrollmentsToCourseDates; @override - final String workflowState; + final String? workflowState; @override - final HomePage homePage; + final HomePage? homePage; @override - final Term term; + final Term? term; @override - final BuiltList
sections; + final BuiltList
? sections; @override - final CourseSettings settings; + final CourseSettings? settings; @override - final BuiltList gradingScheme; + final BuiltList? gradingScheme; - factory _$Course([void Function(CourseBuilder) updates]) => - (new CourseBuilder()..update(updates)).build(); + factory _$Course([void Function(CourseBuilder)? updates]) => + (new CourseBuilder()..update(updates))._build(); _$Course._( {this.currentScore, this.finalScore, this.currentGrade, this.finalGrade, - this.id, - this.name, + required this.id, + required this.name, this.originalName, this.courseCode, this.startAt, this.endAt, this.syllabusBody, - this.hideFinalGrades, - this.isPublic, + required this.hideFinalGrades, + required this.isPublic, this.enrollments, - this.needsGradingCount, - this.applyAssignmentGroupWeights, - this.isFavorite, - this.accessRestrictedByDate, + required this.needsGradingCount, + required this.applyAssignmentGroupWeights, + required this.isFavorite, + required this.accessRestrictedByDate, this.imageDownloadUrl, - this.hasWeightedGradingPeriods, - this.hasGradingPeriods, - this.restrictEnrollmentsToCourseDates, + required this.hasWeightedGradingPeriods, + required this.hasGradingPeriods, + required this.restrictEnrollmentsToCourseDates, this.workflowState, this.homePage, this.term, @@ -393,25 +397,24 @@ class _$Course extends Course { this.settings, this.gradingScheme}) : super._() { - BuiltValueNullFieldError.checkNotNull(id, 'Course', 'id'); - BuiltValueNullFieldError.checkNotNull(name, 'Course', 'name'); + BuiltValueNullFieldError.checkNotNull(id, r'Course', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'Course', 'name'); BuiltValueNullFieldError.checkNotNull( - hideFinalGrades, 'Course', 'hideFinalGrades'); - BuiltValueNullFieldError.checkNotNull(isPublic, 'Course', 'isPublic'); - BuiltValueNullFieldError.checkNotNull(enrollments, 'Course', 'enrollments'); + hideFinalGrades, r'Course', 'hideFinalGrades'); + BuiltValueNullFieldError.checkNotNull(isPublic, r'Course', 'isPublic'); BuiltValueNullFieldError.checkNotNull( - needsGradingCount, 'Course', 'needsGradingCount'); + needsGradingCount, r'Course', 'needsGradingCount'); BuiltValueNullFieldError.checkNotNull( - applyAssignmentGroupWeights, 'Course', 'applyAssignmentGroupWeights'); - BuiltValueNullFieldError.checkNotNull(isFavorite, 'Course', 'isFavorite'); + applyAssignmentGroupWeights, r'Course', 'applyAssignmentGroupWeights'); + BuiltValueNullFieldError.checkNotNull(isFavorite, r'Course', 'isFavorite'); BuiltValueNullFieldError.checkNotNull( - accessRestrictedByDate, 'Course', 'accessRestrictedByDate'); + accessRestrictedByDate, r'Course', 'accessRestrictedByDate'); BuiltValueNullFieldError.checkNotNull( - hasWeightedGradingPeriods, 'Course', 'hasWeightedGradingPeriods'); + hasWeightedGradingPeriods, r'Course', 'hasWeightedGradingPeriods'); BuiltValueNullFieldError.checkNotNull( - hasGradingPeriods, 'Course', 'hasGradingPeriods'); + hasGradingPeriods, r'Course', 'hasGradingPeriods'); BuiltValueNullFieldError.checkNotNull(restrictEnrollmentsToCourseDates, - 'Course', 'restrictEnrollmentsToCourseDates'); + r'Course', 'restrictEnrollmentsToCourseDates'); } @override @@ -458,49 +461,42 @@ class _$Course extends Course { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, currentScore.hashCode), finalScore.hashCode), currentGrade.hashCode), finalGrade.hashCode), id.hashCode), name.hashCode), originalName.hashCode), courseCode.hashCode), startAt.hashCode), - endAt.hashCode), - syllabusBody.hashCode), - hideFinalGrades.hashCode), - isPublic.hashCode), - enrollments.hashCode), - needsGradingCount.hashCode), - applyAssignmentGroupWeights.hashCode), - isFavorite.hashCode), - accessRestrictedByDate.hashCode), - imageDownloadUrl.hashCode), - hasWeightedGradingPeriods.hashCode), - hasGradingPeriods.hashCode), - restrictEnrollmentsToCourseDates.hashCode), - workflowState.hashCode), - homePage.hashCode), - term.hashCode), - sections.hashCode), - settings.hashCode), - gradingScheme.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, currentScore.hashCode); + _$hash = $jc(_$hash, finalScore.hashCode); + _$hash = $jc(_$hash, currentGrade.hashCode); + _$hash = $jc(_$hash, finalGrade.hashCode); + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, originalName.hashCode); + _$hash = $jc(_$hash, courseCode.hashCode); + _$hash = $jc(_$hash, startAt.hashCode); + _$hash = $jc(_$hash, endAt.hashCode); + _$hash = $jc(_$hash, syllabusBody.hashCode); + _$hash = $jc(_$hash, hideFinalGrades.hashCode); + _$hash = $jc(_$hash, isPublic.hashCode); + _$hash = $jc(_$hash, enrollments.hashCode); + _$hash = $jc(_$hash, needsGradingCount.hashCode); + _$hash = $jc(_$hash, applyAssignmentGroupWeights.hashCode); + _$hash = $jc(_$hash, isFavorite.hashCode); + _$hash = $jc(_$hash, accessRestrictedByDate.hashCode); + _$hash = $jc(_$hash, imageDownloadUrl.hashCode); + _$hash = $jc(_$hash, hasWeightedGradingPeriods.hashCode); + _$hash = $jc(_$hash, hasGradingPeriods.hashCode); + _$hash = $jc(_$hash, restrictEnrollmentsToCourseDates.hashCode); + _$hash = $jc(_$hash, workflowState.hashCode); + _$hash = $jc(_$hash, homePage.hashCode); + _$hash = $jc(_$hash, term.hashCode); + _$hash = $jc(_$hash, sections.hashCode); + _$hash = $jc(_$hash, settings.hashCode); + _$hash = $jc(_$hash, gradingScheme.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Course') + return (newBuiltValueToStringHelper(r'Course') ..add('currentScore', currentScore) ..add('finalScore', finalScore) ..add('currentGrade', currentGrade) @@ -535,135 +531,136 @@ class _$Course extends Course { } class CourseBuilder implements Builder { - _$Course _$v; + _$Course? _$v; - double _currentScore; - double get currentScore => _$this._currentScore; - set currentScore(double currentScore) => _$this._currentScore = currentScore; + double? _currentScore; + double? get currentScore => _$this._currentScore; + set currentScore(double? currentScore) => _$this._currentScore = currentScore; - double _finalScore; - double get finalScore => _$this._finalScore; - set finalScore(double finalScore) => _$this._finalScore = finalScore; + double? _finalScore; + double? get finalScore => _$this._finalScore; + set finalScore(double? finalScore) => _$this._finalScore = finalScore; - String _currentGrade; - String get currentGrade => _$this._currentGrade; - set currentGrade(String currentGrade) => _$this._currentGrade = currentGrade; + String? _currentGrade; + String? get currentGrade => _$this._currentGrade; + set currentGrade(String? currentGrade) => _$this._currentGrade = currentGrade; - String _finalGrade; - String get finalGrade => _$this._finalGrade; - set finalGrade(String finalGrade) => _$this._finalGrade = finalGrade; + String? _finalGrade; + String? get finalGrade => _$this._finalGrade; + set finalGrade(String? finalGrade) => _$this._finalGrade = finalGrade; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _originalName; - String get originalName => _$this._originalName; - set originalName(String originalName) => _$this._originalName = originalName; + String? _originalName; + String? get originalName => _$this._originalName; + set originalName(String? originalName) => _$this._originalName = originalName; - String _courseCode; - String get courseCode => _$this._courseCode; - set courseCode(String courseCode) => _$this._courseCode = courseCode; + String? _courseCode; + String? get courseCode => _$this._courseCode; + set courseCode(String? courseCode) => _$this._courseCode = courseCode; - DateTime _startAt; - DateTime get startAt => _$this._startAt; - set startAt(DateTime startAt) => _$this._startAt = startAt; + DateTime? _startAt; + DateTime? get startAt => _$this._startAt; + set startAt(DateTime? startAt) => _$this._startAt = startAt; - DateTime _endAt; - DateTime get endAt => _$this._endAt; - set endAt(DateTime endAt) => _$this._endAt = endAt; + DateTime? _endAt; + DateTime? get endAt => _$this._endAt; + set endAt(DateTime? endAt) => _$this._endAt = endAt; - String _syllabusBody; - String get syllabusBody => _$this._syllabusBody; - set syllabusBody(String syllabusBody) => _$this._syllabusBody = syllabusBody; + String? _syllabusBody; + String? get syllabusBody => _$this._syllabusBody; + set syllabusBody(String? syllabusBody) => _$this._syllabusBody = syllabusBody; - bool _hideFinalGrades; - bool get hideFinalGrades => _$this._hideFinalGrades; - set hideFinalGrades(bool hideFinalGrades) => + bool? _hideFinalGrades; + bool? get hideFinalGrades => _$this._hideFinalGrades; + set hideFinalGrades(bool? hideFinalGrades) => _$this._hideFinalGrades = hideFinalGrades; - bool _isPublic; - bool get isPublic => _$this._isPublic; - set isPublic(bool isPublic) => _$this._isPublic = isPublic; + bool? _isPublic; + bool? get isPublic => _$this._isPublic; + set isPublic(bool? isPublic) => _$this._isPublic = isPublic; - ListBuilder _enrollments; + ListBuilder? _enrollments; ListBuilder get enrollments => _$this._enrollments ??= new ListBuilder(); - set enrollments(ListBuilder enrollments) => + set enrollments(ListBuilder? enrollments) => _$this._enrollments = enrollments; - int _needsGradingCount; - int get needsGradingCount => _$this._needsGradingCount; - set needsGradingCount(int needsGradingCount) => + int? _needsGradingCount; + int? get needsGradingCount => _$this._needsGradingCount; + set needsGradingCount(int? needsGradingCount) => _$this._needsGradingCount = needsGradingCount; - bool _applyAssignmentGroupWeights; - bool get applyAssignmentGroupWeights => _$this._applyAssignmentGroupWeights; - set applyAssignmentGroupWeights(bool applyAssignmentGroupWeights) => + bool? _applyAssignmentGroupWeights; + bool? get applyAssignmentGroupWeights => _$this._applyAssignmentGroupWeights; + set applyAssignmentGroupWeights(bool? applyAssignmentGroupWeights) => _$this._applyAssignmentGroupWeights = applyAssignmentGroupWeights; - bool _isFavorite; - bool get isFavorite => _$this._isFavorite; - set isFavorite(bool isFavorite) => _$this._isFavorite = isFavorite; + bool? _isFavorite; + bool? get isFavorite => _$this._isFavorite; + set isFavorite(bool? isFavorite) => _$this._isFavorite = isFavorite; - bool _accessRestrictedByDate; - bool get accessRestrictedByDate => _$this._accessRestrictedByDate; - set accessRestrictedByDate(bool accessRestrictedByDate) => + bool? _accessRestrictedByDate; + bool? get accessRestrictedByDate => _$this._accessRestrictedByDate; + set accessRestrictedByDate(bool? accessRestrictedByDate) => _$this._accessRestrictedByDate = accessRestrictedByDate; - String _imageDownloadUrl; - String get imageDownloadUrl => _$this._imageDownloadUrl; - set imageDownloadUrl(String imageDownloadUrl) => + String? _imageDownloadUrl; + String? get imageDownloadUrl => _$this._imageDownloadUrl; + set imageDownloadUrl(String? imageDownloadUrl) => _$this._imageDownloadUrl = imageDownloadUrl; - bool _hasWeightedGradingPeriods; - bool get hasWeightedGradingPeriods => _$this._hasWeightedGradingPeriods; - set hasWeightedGradingPeriods(bool hasWeightedGradingPeriods) => + bool? _hasWeightedGradingPeriods; + bool? get hasWeightedGradingPeriods => _$this._hasWeightedGradingPeriods; + set hasWeightedGradingPeriods(bool? hasWeightedGradingPeriods) => _$this._hasWeightedGradingPeriods = hasWeightedGradingPeriods; - bool _hasGradingPeriods; - bool get hasGradingPeriods => _$this._hasGradingPeriods; - set hasGradingPeriods(bool hasGradingPeriods) => + bool? _hasGradingPeriods; + bool? get hasGradingPeriods => _$this._hasGradingPeriods; + set hasGradingPeriods(bool? hasGradingPeriods) => _$this._hasGradingPeriods = hasGradingPeriods; - bool _restrictEnrollmentsToCourseDates; - bool get restrictEnrollmentsToCourseDates => + bool? _restrictEnrollmentsToCourseDates; + bool? get restrictEnrollmentsToCourseDates => _$this._restrictEnrollmentsToCourseDates; - set restrictEnrollmentsToCourseDates(bool restrictEnrollmentsToCourseDates) => + set restrictEnrollmentsToCourseDates( + bool? restrictEnrollmentsToCourseDates) => _$this._restrictEnrollmentsToCourseDates = restrictEnrollmentsToCourseDates; - String _workflowState; - String get workflowState => _$this._workflowState; - set workflowState(String workflowState) => + String? _workflowState; + String? get workflowState => _$this._workflowState; + set workflowState(String? workflowState) => _$this._workflowState = workflowState; - HomePage _homePage; - HomePage get homePage => _$this._homePage; - set homePage(HomePage homePage) => _$this._homePage = homePage; + HomePage? _homePage; + HomePage? get homePage => _$this._homePage; + set homePage(HomePage? homePage) => _$this._homePage = homePage; - TermBuilder _term; + TermBuilder? _term; TermBuilder get term => _$this._term ??= new TermBuilder(); - set term(TermBuilder term) => _$this._term = term; + set term(TermBuilder? term) => _$this._term = term; - ListBuilder
_sections; + ListBuilder
? _sections; ListBuilder
get sections => _$this._sections ??= new ListBuilder
(); - set sections(ListBuilder
sections) => _$this._sections = sections; + set sections(ListBuilder
? sections) => _$this._sections = sections; - CourseSettingsBuilder _settings; + CourseSettingsBuilder? _settings; CourseSettingsBuilder get settings => _$this._settings ??= new CourseSettingsBuilder(); - set settings(CourseSettingsBuilder settings) => _$this._settings = settings; + set settings(CourseSettingsBuilder? settings) => _$this._settings = settings; - ListBuilder _gradingScheme; + ListBuilder? _gradingScheme; ListBuilder get gradingScheme => _$this._gradingScheme ??= new ListBuilder(); - set gradingScheme(ListBuilder gradingScheme) => + set gradingScheme(ListBuilder? gradingScheme) => _$this._gradingScheme = gradingScheme; CourseBuilder() { @@ -686,7 +683,7 @@ class CourseBuilder implements Builder { _syllabusBody = $v.syllabusBody; _hideFinalGrades = $v.hideFinalGrades; _isPublic = $v.isPublic; - _enrollments = $v.enrollments.toBuilder(); + _enrollments = $v.enrollments?.toBuilder(); _needsGradingCount = $v.needsGradingCount; _applyAssignmentGroupWeights = $v.applyAssignmentGroupWeights; _isFavorite = $v.isFavorite; @@ -713,12 +710,14 @@ class CourseBuilder implements Builder { } @override - void update(void Function(CourseBuilder) updates) { + void update(void Function(CourseBuilder)? updates) { if (updates != null) updates(this); } @override - _$Course build() { + Course build() => _build(); + + _$Course _build() { _$Course _$result; try { _$result = _$v ?? @@ -727,33 +726,33 @@ class CourseBuilder implements Builder { finalScore: finalScore, currentGrade: currentGrade, finalGrade: finalGrade, - id: BuiltValueNullFieldError.checkNotNull(id, 'Course', 'id'), - name: - BuiltValueNullFieldError.checkNotNull(name, 'Course', 'name'), + id: BuiltValueNullFieldError.checkNotNull(id, r'Course', 'id'), + name: BuiltValueNullFieldError.checkNotNull( + name, r'Course', 'name'), originalName: originalName, courseCode: courseCode, startAt: startAt, endAt: endAt, syllabusBody: syllabusBody, hideFinalGrades: BuiltValueNullFieldError.checkNotNull( - hideFinalGrades, 'Course', 'hideFinalGrades'), + hideFinalGrades, r'Course', 'hideFinalGrades'), isPublic: BuiltValueNullFieldError.checkNotNull( - isPublic, 'Course', 'isPublic'), - enrollments: enrollments.build(), + isPublic, r'Course', 'isPublic'), + enrollments: _enrollments?.build(), needsGradingCount: BuiltValueNullFieldError.checkNotNull( - needsGradingCount, 'Course', 'needsGradingCount'), + needsGradingCount, r'Course', 'needsGradingCount'), applyAssignmentGroupWeights: BuiltValueNullFieldError.checkNotNull( - applyAssignmentGroupWeights, 'Course', 'applyAssignmentGroupWeights'), + applyAssignmentGroupWeights, r'Course', 'applyAssignmentGroupWeights'), isFavorite: BuiltValueNullFieldError.checkNotNull( - isFavorite, 'Course', 'isFavorite'), + isFavorite, r'Course', 'isFavorite'), accessRestrictedByDate: BuiltValueNullFieldError.checkNotNull( - accessRestrictedByDate, 'Course', 'accessRestrictedByDate'), + accessRestrictedByDate, r'Course', 'accessRestrictedByDate'), imageDownloadUrl: imageDownloadUrl, hasWeightedGradingPeriods: BuiltValueNullFieldError.checkNotNull( - hasWeightedGradingPeriods, 'Course', 'hasWeightedGradingPeriods'), + hasWeightedGradingPeriods, r'Course', 'hasWeightedGradingPeriods'), hasGradingPeriods: - BuiltValueNullFieldError.checkNotNull(hasGradingPeriods, 'Course', 'hasGradingPeriods'), - restrictEnrollmentsToCourseDates: BuiltValueNullFieldError.checkNotNull(restrictEnrollmentsToCourseDates, 'Course', 'restrictEnrollmentsToCourseDates'), + BuiltValueNullFieldError.checkNotNull(hasGradingPeriods, r'Course', 'hasGradingPeriods'), + restrictEnrollmentsToCourseDates: BuiltValueNullFieldError.checkNotNull(restrictEnrollmentsToCourseDates, r'Course', 'restrictEnrollmentsToCourseDates'), workflowState: workflowState, homePage: homePage, term: _term?.build(), @@ -761,10 +760,10 @@ class CourseBuilder implements Builder { settings: _settings?.build(), gradingScheme: _gradingScheme?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'enrollments'; - enrollments.build(); + _enrollments?.build(); _$failedField = 'term'; _term?.build(); @@ -776,7 +775,7 @@ class CourseBuilder implements Builder { _gradingScheme?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Course', _$failedField, e.toString()); + r'Course', _$failedField, e.toString()); } rethrow; } @@ -785,4 +784,4 @@ class CourseBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/course_grade.dart b/apps/flutter_parent/lib/models/course_grade.dart index 438bf11ca5..dfe9b14b76 100644 --- a/apps/flutter_parent/lib/models/course_grade.dart +++ b/apps/flutter_parent/lib/models/course_grade.dart @@ -33,8 +33,8 @@ import 'enrollment.dart'; * represented in the UI with "N/A". See Course.noFinalGrade for logic. */ class CourseGrade { - Course _course; - Enrollment _enrollment; + Course? _course; + Enrollment? _enrollment; bool _forceAllPeriods; CourseGrade(this._course, this._enrollment, {bool forceAllPeriods = false}) : _forceAllPeriods = forceAllPeriods; @@ -43,7 +43,7 @@ class CourseGrade { if (!(other is CourseGrade)) { return false; } - final grade = other as CourseGrade; + final grade = other; return _course == grade._course && _enrollment == grade._enrollment && _forceAllPeriods == grade._forceAllPeriods; } @@ -61,44 +61,31 @@ class CourseGrade { /// Current score value, a double representation of a percentage grade, for the current grading period or the current /// term (see Course.getCourseGrade, ignoreMGP). Needs formatting prior to use. - double currentScore() => _hasActiveGradingPeriod() ? _getCurrentPeriodComputedCurrentScore() : _getCurrentScore(); + double? currentScore() => _hasActiveGradingPeriod() ? _getCurrentPeriodComputedCurrentScore() : _getCurrentScore(); /// Current grade string value, for the current grading period or the current term. (see Course.getCourseGrade) - String currentGrade() => _hasActiveGradingPeriod() ? _getCurrentPeriodComputedCurrentGrade() : _getCurrentGrade(); + String? currentGrade() => _hasActiveGradingPeriod() ? _getCurrentPeriodComputedCurrentGrade() : _getCurrentGrade(); /// If the course contains no valid current grade or score, this flag will be true. This is usually represented in the /// UI with "N/A". bool noCurrentGrade() => - currentScore() == null && (currentGrade() == null || currentGrade().contains('N/A') || currentGrade().isEmpty); + _getCurrentScore() == null && (currentGrade() == null || currentGrade()!.contains('N/A') || currentGrade()!.isEmpty); bool _hasActiveGradingPeriod() => !_forceAllPeriods && - (_course?.enrollments?.toList()?.any((enrollment) => enrollment.hasActiveGradingPeriod()) ?? false); + (_course?.enrollments?.toList().any((enrollment) => enrollment.hasActiveGradingPeriod()) ?? false); bool _isTotalsForAllGradingPeriodsEnabled() => - _course?.enrollments?.toList()?.any((enrollment) => enrollment.isTotalsForAllGradingPeriodsEnabled()) ?? false; + _course?.enrollments?.toList().any((enrollment) => enrollment.isTotalsForAllGradingPeriodsEnabled()) ?? false; - double _getCurrentScore() => _enrollment?.grades?.currentScore ?? _enrollment?.computedCurrentScore; + double? _getCurrentScore() => _enrollment?.grades?.currentScore ?? _enrollment?.computedCurrentScore; -// double _getFinalScore() => -// _enrollment.grade?.finalScore ?? _enrollment.computedFinalScore; + String? _getCurrentGrade() => _enrollment?.grades?.currentGrade ?? _enrollment?.computedCurrentGrade ?? _enrollment?.computedCurrentLetterGrade; - String _getCurrentGrade() => _enrollment?.grades?.currentGrade ?? _enrollment?.computedCurrentGrade ?? _enrollment?.computedCurrentLetterGrade; - -// String _getFinalGrade() => -// _enrollment.grade?.finalGrade ?? _enrollment.computedFinalGrade; - - double _getCurrentPeriodComputedCurrentScore() => + double? _getCurrentPeriodComputedCurrentScore() => _enrollment?.grades?.currentScore ?? _enrollment?.currentPeriodComputedCurrentScore; - String _getCurrentPeriodComputedCurrentGrade() => + String? _getCurrentPeriodComputedCurrentGrade() => _enrollment?.grades?.currentGrade ?? _enrollment?.currentPeriodComputedCurrentGrade; -// double _getCurrentPeriodComputedFinalScore() => -// _enrollment.grade?.finalScore ?? -// _enrollment.currentPeriodComputedFinalScore; - -// String _getCurrentPeriodComputedFinalGrade() => -// _enrollment.grade?.finalGrade ?? -// _enrollment.currentPeriodComputedFinalGrade; } diff --git a/apps/flutter_parent/lib/models/course_permissions.dart b/apps/flutter_parent/lib/models/course_permissions.dart index 757127d36b..0222c04e9d 100644 --- a/apps/flutter_parent/lib/models/course_permissions.dart +++ b/apps/flutter_parent/lib/models/course_permissions.dart @@ -22,9 +22,8 @@ part 'course_permissions.g.dart'; abstract class CoursePermissions implements Built { static Serializer get serializer => _$coursePermissionsSerializer; - @nullable @BuiltValueField(wireName: 'send_messages') - bool get sendMessages; + bool? get sendMessages; CoursePermissions._(); factory CoursePermissions([void Function(CoursePermissionsBuilder) updates]) = _$CoursePermissions; diff --git a/apps/flutter_parent/lib/models/course_permissions.g.dart b/apps/flutter_parent/lib/models/course_permissions.g.dart index 5dbfa7852b..d5d33dd74b 100644 --- a/apps/flutter_parent/lib/models/course_permissions.g.dart +++ b/apps/flutter_parent/lib/models/course_permissions.g.dart @@ -17,33 +17,35 @@ class _$CoursePermissionsSerializer final String wireName = 'CoursePermissions'; @override - Iterable serialize(Serializers serializers, CoursePermissions object, + Iterable serialize(Serializers serializers, CoursePermissions object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - if (object.sendMessages != null) { + final result = []; + Object? value; + value = object.sendMessages; + if (value != null) { result ..add('send_messages') - ..add(serializers.serialize(object.sendMessages, - specifiedType: const FullType(bool))); + ..add( + serializers.serialize(value, specifiedType: const FullType(bool))); } return result; } @override CoursePermissions deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CoursePermissionsBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'send_messages': result.sendMessages = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; } } @@ -54,11 +56,11 @@ class _$CoursePermissionsSerializer class _$CoursePermissions extends CoursePermissions { @override - final bool sendMessages; + final bool? sendMessages; factory _$CoursePermissions( - [void Function(CoursePermissionsBuilder) updates]) => - (new CoursePermissionsBuilder()..update(updates)).build(); + [void Function(CoursePermissionsBuilder)? updates]) => + (new CoursePermissionsBuilder()..update(updates))._build(); _$CoursePermissions._({this.sendMessages}) : super._(); @@ -78,12 +80,15 @@ class _$CoursePermissions extends CoursePermissions { @override int get hashCode { - return $jf($jc(0, sendMessages.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, sendMessages.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CoursePermissions') + return (newBuiltValueToStringHelper(r'CoursePermissions') ..add('sendMessages', sendMessages)) .toString(); } @@ -91,17 +96,18 @@ class _$CoursePermissions extends CoursePermissions { class CoursePermissionsBuilder implements Builder { - _$CoursePermissions _$v; + _$CoursePermissions? _$v; - bool _sendMessages; - bool get sendMessages => _$this._sendMessages; - set sendMessages(bool sendMessages) => _$this._sendMessages = sendMessages; + bool? _sendMessages; + bool? get sendMessages => _$this._sendMessages; + set sendMessages(bool? sendMessages) => _$this._sendMessages = sendMessages; CoursePermissionsBuilder(); CoursePermissionsBuilder get _$this { - if (_$v != null) { - _sendMessages = _$v.sendMessages; + final $v = _$v; + if ($v != null) { + _sendMessages = $v.sendMessages; _$v = null; } return this; @@ -109,19 +115,19 @@ class CoursePermissionsBuilder @override void replace(CoursePermissions other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CoursePermissions; } @override - void update(void Function(CoursePermissionsBuilder) updates) { + void update(void Function(CoursePermissionsBuilder)? updates) { if (updates != null) updates(this); } @override - _$CoursePermissions build() { + CoursePermissions build() => _build(); + + _$CoursePermissions _build() { final _$result = _$v ?? new _$CoursePermissions._(sendMessages: sendMessages); replace(_$result); @@ -129,4 +135,4 @@ class CoursePermissionsBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/course_settings.dart b/apps/flutter_parent/lib/models/course_settings.dart index 6764896326..d869457cde 100644 --- a/apps/flutter_parent/lib/models/course_settings.dart +++ b/apps/flutter_parent/lib/models/course_settings.dart @@ -23,12 +23,10 @@ abstract class CourseSettings implements Built get serializer => _$courseSettingsSerializer; @BuiltValueField(wireName: 'syllabus_course_summary') - @nullable - bool get courseSummary; + bool? get courseSummary; @BuiltValueField(wireName: 'restrict_quantitative_data') - @nullable - bool get restrictQuantitativeData; + bool? get restrictQuantitativeData; CourseSettings._(); factory CourseSettings([void Function(CourseSettingsBuilder) updates]) = _$CourseSettings; diff --git a/apps/flutter_parent/lib/models/course_settings.g.dart b/apps/flutter_parent/lib/models/course_settings.g.dart index baac66604c..5a12d8572f 100644 --- a/apps/flutter_parent/lib/models/course_settings.g.dart +++ b/apps/flutter_parent/lib/models/course_settings.g.dart @@ -17,10 +17,10 @@ class _$CourseSettingsSerializer final String wireName = 'CourseSettings'; @override - Iterable serialize(Serializers serializers, CourseSettings object, + Iterable serialize(Serializers serializers, CourseSettings object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - Object value; + final result = []; + Object? value; value = object.courseSummary; if (value != null) { result @@ -40,23 +40,23 @@ class _$CourseSettingsSerializer @override CourseSettings deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CourseSettingsBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'syllabus_course_summary': result.courseSummary = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'restrict_quantitative_data': result.restrictQuantitativeData = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; } } @@ -67,12 +67,12 @@ class _$CourseSettingsSerializer class _$CourseSettings extends CourseSettings { @override - final bool courseSummary; + final bool? courseSummary; @override - final bool restrictQuantitativeData; + final bool? restrictQuantitativeData; - factory _$CourseSettings([void Function(CourseSettingsBuilder) updates]) => - (new CourseSettingsBuilder()..update(updates)).build(); + factory _$CourseSettings([void Function(CourseSettingsBuilder)? updates]) => + (new CourseSettingsBuilder()..update(updates))._build(); _$CourseSettings._({this.courseSummary, this.restrictQuantitativeData}) : super._(); @@ -95,13 +95,16 @@ class _$CourseSettings extends CourseSettings { @override int get hashCode { - return $jf( - $jc($jc(0, courseSummary.hashCode), restrictQuantitativeData.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, courseSummary.hashCode); + _$hash = $jc(_$hash, restrictQuantitativeData.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CourseSettings') + return (newBuiltValueToStringHelper(r'CourseSettings') ..add('courseSummary', courseSummary) ..add('restrictQuantitativeData', restrictQuantitativeData)) .toString(); @@ -110,16 +113,16 @@ class _$CourseSettings extends CourseSettings { class CourseSettingsBuilder implements Builder { - _$CourseSettings _$v; + _$CourseSettings? _$v; - bool _courseSummary; - bool get courseSummary => _$this._courseSummary; - set courseSummary(bool courseSummary) => + bool? _courseSummary; + bool? get courseSummary => _$this._courseSummary; + set courseSummary(bool? courseSummary) => _$this._courseSummary = courseSummary; - bool _restrictQuantitativeData; - bool get restrictQuantitativeData => _$this._restrictQuantitativeData; - set restrictQuantitativeData(bool restrictQuantitativeData) => + bool? _restrictQuantitativeData; + bool? get restrictQuantitativeData => _$this._restrictQuantitativeData; + set restrictQuantitativeData(bool? restrictQuantitativeData) => _$this._restrictQuantitativeData = restrictQuantitativeData; CourseSettingsBuilder(); @@ -141,12 +144,14 @@ class CourseSettingsBuilder } @override - void update(void Function(CourseSettingsBuilder) updates) { + void update(void Function(CourseSettingsBuilder)? updates) { if (updates != null) updates(this); } @override - _$CourseSettings build() { + CourseSettings build() => _build(); + + _$CourseSettings _build() { final _$result = _$v ?? new _$CourseSettings._( courseSummary: courseSummary, @@ -156,4 +161,4 @@ class CourseSettingsBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/course_tab.g.dart b/apps/flutter_parent/lib/models/course_tab.g.dart index 477a37a230..aca82c6850 100644 --- a/apps/flutter_parent/lib/models/course_tab.g.dart +++ b/apps/flutter_parent/lib/models/course_tab.g.dart @@ -15,9 +15,9 @@ class _$CourseTabSerializer implements StructuredSerializer { final String wireName = 'CourseTab'; @override - Iterable serialize(Serializers serializers, CourseTab object, + Iterable serialize(Serializers serializers, CourseTab object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), ]; @@ -26,20 +26,19 @@ class _$CourseTabSerializer implements StructuredSerializer { } @override - CourseTab deserialize(Serializers serializers, Iterable serialized, + CourseTab deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CourseTabBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -52,13 +51,11 @@ class _$CourseTab extends CourseTab { @override final String id; - factory _$CourseTab([void Function(CourseTabBuilder) updates]) => - (new CourseTabBuilder()..update(updates)).build(); + factory _$CourseTab([void Function(CourseTabBuilder)? updates]) => + (new CourseTabBuilder()..update(updates))._build(); - _$CourseTab._({this.id}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('CourseTab', 'id'); - } + _$CourseTab._({required this.id}) : super._() { + BuiltValueNullFieldError.checkNotNull(id, r'CourseTab', 'id'); } @override @@ -76,27 +73,32 @@ class _$CourseTab extends CourseTab { @override int get hashCode { - return $jf($jc(0, id.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CourseTab')..add('id', id)).toString(); + return (newBuiltValueToStringHelper(r'CourseTab')..add('id', id)) + .toString(); } } class CourseTabBuilder implements Builder { - _$CourseTab _$v; + _$CourseTab? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; CourseTabBuilder(); CourseTabBuilder get _$this { - if (_$v != null) { - _id = _$v.id; + final $v = _$v; + if ($v != null) { + _id = $v.id; _$v = null; } return this; @@ -104,23 +106,25 @@ class CourseTabBuilder implements Builder { @override void replace(CourseTab other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CourseTab; } @override - void update(void Function(CourseTabBuilder) updates) { + void update(void Function(CourseTabBuilder)? updates) { if (updates != null) updates(this); } @override - _$CourseTab build() { - final _$result = _$v ?? new _$CourseTab._(id: id); + CourseTab build() => _build(); + + _$CourseTab _build() { + final _$result = _$v ?? + new _$CourseTab._( + id: BuiltValueNullFieldError.checkNotNull(id, r'CourseTab', 'id')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/communication_channel.g.dart b/apps/flutter_parent/lib/models/dataseeding/communication_channel.g.dart index 5c6900d3fc..be18d24237 100644 --- a/apps/flutter_parent/lib/models/dataseeding/communication_channel.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/communication_channel.g.dart @@ -20,10 +20,10 @@ class _$CommunicationChannelSerializer final String wireName = 'CommunicationChannel'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CommunicationChannel object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'skip_confirmation', serializers.serialize(object.skipConfirmation, specifiedType: const FullType(bool)), @@ -34,20 +34,19 @@ class _$CommunicationChannelSerializer @override CommunicationChannel deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CommunicationChannelBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'skip_confirmation': result.skipConfirmation = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -61,14 +60,12 @@ class _$CommunicationChannel extends CommunicationChannel { final bool skipConfirmation; factory _$CommunicationChannel( - [void Function(CommunicationChannelBuilder) updates]) => - (new CommunicationChannelBuilder()..update(updates)).build(); + [void Function(CommunicationChannelBuilder)? updates]) => + (new CommunicationChannelBuilder()..update(updates))._build(); - _$CommunicationChannel._({this.skipConfirmation}) : super._() { - if (skipConfirmation == null) { - throw new BuiltValueNullFieldError( - 'CommunicationChannel', 'skipConfirmation'); - } + _$CommunicationChannel._({required this.skipConfirmation}) : super._() { + BuiltValueNullFieldError.checkNotNull( + skipConfirmation, r'CommunicationChannel', 'skipConfirmation'); } @override @@ -89,12 +86,15 @@ class _$CommunicationChannel extends CommunicationChannel { @override int get hashCode { - return $jf($jc(0, skipConfirmation.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, skipConfirmation.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CommunicationChannel') + return (newBuiltValueToStringHelper(r'CommunicationChannel') ..add('skipConfirmation', skipConfirmation)) .toString(); } @@ -102,11 +102,11 @@ class _$CommunicationChannel extends CommunicationChannel { class CommunicationChannelBuilder implements Builder { - _$CommunicationChannel _$v; + _$CommunicationChannel? _$v; - bool _skipConfirmation; - bool get skipConfirmation => _$this._skipConfirmation; - set skipConfirmation(bool skipConfirmation) => + bool? _skipConfirmation; + bool? get skipConfirmation => _$this._skipConfirmation; + set skipConfirmation(bool? skipConfirmation) => _$this._skipConfirmation = skipConfirmation; CommunicationChannelBuilder() { @@ -114,8 +114,9 @@ class CommunicationChannelBuilder } CommunicationChannelBuilder get _$this { - if (_$v != null) { - _skipConfirmation = _$v.skipConfirmation; + final $v = _$v; + if ($v != null) { + _skipConfirmation = $v.skipConfirmation; _$v = null; } return this; @@ -123,24 +124,26 @@ class CommunicationChannelBuilder @override void replace(CommunicationChannel other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CommunicationChannel; } @override - void update(void Function(CommunicationChannelBuilder) updates) { + void update(void Function(CommunicationChannelBuilder)? updates) { if (updates != null) updates(this); } @override - _$CommunicationChannel build() { - final _$result = - _$v ?? new _$CommunicationChannel._(skipConfirmation: skipConfirmation); + CommunicationChannel build() => _build(); + + _$CommunicationChannel _build() { + final _$result = _$v ?? + new _$CommunicationChannel._( + skipConfirmation: BuiltValueNullFieldError.checkNotNull( + skipConfirmation, r'CommunicationChannel', 'skipConfirmation')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.dart b/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.dart index 17dfa47af9..23b2195823 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.dart @@ -30,12 +30,10 @@ abstract class CreateAssignmentInfo implements Built get submissionTypes; + BuiltList? get submissionTypes; static void _initializeBuilder(CreateAssignmentInfoBuilder b) => b; - bool get isDiscussion => submissionTypes.contains(SubmissionTypes.discussionTopic); + bool get isDiscussion => submissionTypes?.contains(SubmissionTypes.discussionTopic) == true; - bool get isQuiz => submissionTypes.contains(SubmissionTypes.onlineQuiz); + bool get isQuiz => submissionTypes?.contains(SubmissionTypes.onlineQuiz) == true; } diff --git a/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.g.dart index c73d638b1d..9322ddc24c 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_assignment_info.g.dart @@ -20,10 +20,10 @@ class _$CreateAssignmentInfoSerializer final String wireName = 'CreateAssignmentInfo'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateAssignmentInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), 'points_possible', @@ -36,238 +36,216 @@ class _$CreateAssignmentInfoSerializer serializers.serialize(object.published, specifiedType: const FullType(bool)), ]; - result.add('description'); - if (object.description == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.description, - specifiedType: const FullType(String))); - } - result.add('due_at'); - if (object.dueAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.dueAt, + Object? value; + value = object.description; + + result + ..add('description') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.dueAt; + + result + ..add('due_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('grading_type'); - if (object.gradingType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.gradingType, + value = object.gradingType; + + result + ..add('grading_type') + ..add(serializers.serialize(value, specifiedType: const FullType(GradingType))); - } - result.add('html_url'); - if (object.htmlUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.htmlUrl, - specifiedType: const FullType(String))); - } - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('quiz_id'); - if (object.quizId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.quizId, - specifiedType: const FullType(String))); - } - result.add('use_rubric_for_grading'); - if (object.useRubricForGrading == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.useRubricForGrading, - specifiedType: const FullType(bool))); - } - result.add('assignment_group_id'); - if (object.assignmentGroupId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.assignmentGroupId, - specifiedType: const FullType(String))); - } - result.add('position'); - if (object.position == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.position, - specifiedType: const FullType(int))); - } - result.add('lock_info'); - if (object.lockInfo == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockInfo, + value = object.htmlUrl; + + result + ..add('html_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.quizId; + + result + ..add('quiz_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.useRubricForGrading; + + result + ..add('use_rubric_for_grading') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.assignmentGroupId; + + result + ..add('assignment_group_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.position; + + result + ..add('position') + ..add(serializers.serialize(value, specifiedType: const FullType(int))); + value = object.lockInfo; + + result + ..add('lock_info') + ..add(serializers.serialize(value, specifiedType: const FullType(LockInfo))); - } - result.add('locked_for_user'); - if (object.lockedForUser == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockedForUser, - specifiedType: const FullType(bool))); - } - result.add('lock_at'); - if (object.lockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockAt, + value = object.lockedForUser; + + result + ..add('locked_for_user') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.lockAt; + + result + ..add('lock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('unlock_at'); - if (object.unlockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.unlockAt, + value = object.unlockAt; + + result + ..add('unlock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('lock_explanation'); - if (object.lockExplanation == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.lockExplanation, - specifiedType: const FullType(String))); - } - result.add('free_form_criterion_comments'); - if (object.freeFormCriterionComments == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.freeFormCriterionComments, - specifiedType: const FullType(bool))); - } - result.add('muted'); - if (object.muted == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.muted, - specifiedType: const FullType(bool))); - } - result.add('group_category_id'); - if (object.groupCategoryId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.groupCategoryId, - specifiedType: const FullType(String))); - } - result.add('submission_types'); - if (object.submissionTypes == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionTypes, + value = object.lockExplanation; + + result + ..add('lock_explanation') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.freeFormCriterionComments; + + result + ..add('free_form_criterion_comments') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.muted; + + result + ..add('muted') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.groupCategoryId; + + result + ..add('group_category_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.submissionTypes; + + result + ..add('submission_types') + ..add(serializers.serialize(value, specifiedType: const FullType( BuiltList, const [const FullType(SubmissionTypes)]))); - } + return result; } @override CreateAssignmentInfo deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateAssignmentInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'description': result.description = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'due_at': result.dueAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'points_possible': result.pointsPossible = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double))! as double; break; case 'course_id': result.courseId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'grading_type': result.gradingType = serializers.deserialize(value, - specifiedType: const FullType(GradingType)) as GradingType; + specifiedType: const FullType(GradingType)) as GradingType?; break; case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'quiz_id': result.quizId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'use_rubric_for_grading': result.useRubricForGrading = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'assignment_group_id': result.assignmentGroupId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'position': result.position = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int)) as int?; break; case 'lock_info': result.lockInfo.replace(serializers.deserialize(value, - specifiedType: const FullType(LockInfo)) as LockInfo); + specifiedType: const FullType(LockInfo))! as LockInfo); break; case 'locked_for_user': result.lockedForUser = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'lock_at': result.lockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'unlock_at': result.unlockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'lock_explanation': result.lockExplanation = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'free_form_criterion_comments': result.freeFormCriterionComments = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'published': result.published = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'muted': result.muted = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'group_category_id': result.groupCategoryId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'submission_types': result.submissionTypes.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(SubmissionTypes)])) - as BuiltList); + BuiltList, const [const FullType(SubmissionTypes)]))! + as BuiltList); break; } } @@ -280,58 +258,58 @@ class _$CreateAssignmentInfo extends CreateAssignmentInfo { @override final String name; @override - final String description; + final String? description; @override - final DateTime dueAt; + final DateTime? dueAt; @override final double pointsPossible; @override final String courseId; @override - final GradingType gradingType; + final GradingType? gradingType; @override - final String htmlUrl; + final String? htmlUrl; @override - final String url; + final String? url; @override - final String quizId; + final String? quizId; @override - final bool useRubricForGrading; + final bool? useRubricForGrading; @override - final String assignmentGroupId; + final String? assignmentGroupId; @override - final int position; + final int? position; @override - final LockInfo lockInfo; + final LockInfo? lockInfo; @override - final bool lockedForUser; + final bool? lockedForUser; @override - final DateTime lockAt; + final DateTime? lockAt; @override - final DateTime unlockAt; + final DateTime? unlockAt; @override - final String lockExplanation; + final String? lockExplanation; @override - final bool freeFormCriterionComments; + final bool? freeFormCriterionComments; @override final bool published; @override - final bool muted; + final bool? muted; @override - final String groupCategoryId; + final String? groupCategoryId; @override - final BuiltList submissionTypes; + final BuiltList? submissionTypes; factory _$CreateAssignmentInfo( - [void Function(CreateAssignmentInfoBuilder) updates]) => - (new CreateAssignmentInfoBuilder()..update(updates)).build(); + [void Function(CreateAssignmentInfoBuilder)? updates]) => + (new CreateAssignmentInfoBuilder()..update(updates))._build(); _$CreateAssignmentInfo._( - {this.name, + {required this.name, this.description, this.dueAt, - this.pointsPossible, - this.courseId, + required this.pointsPossible, + required this.courseId, this.gradingType, this.htmlUrl, this.url, @@ -345,24 +323,19 @@ class _$CreateAssignmentInfo extends CreateAssignmentInfo { this.unlockAt, this.lockExplanation, this.freeFormCriterionComments, - this.published, + required this.published, this.muted, this.groupCategoryId, this.submissionTypes}) : super._() { - if (name == null) { - throw new BuiltValueNullFieldError('CreateAssignmentInfo', 'name'); - } - if (pointsPossible == null) { - throw new BuiltValueNullFieldError( - 'CreateAssignmentInfo', 'pointsPossible'); - } - if (courseId == null) { - throw new BuiltValueNullFieldError('CreateAssignmentInfo', 'courseId'); - } - if (published == null) { - throw new BuiltValueNullFieldError('CreateAssignmentInfo', 'published'); - } + BuiltValueNullFieldError.checkNotNull( + name, r'CreateAssignmentInfo', 'name'); + BuiltValueNullFieldError.checkNotNull( + pointsPossible, r'CreateAssignmentInfo', 'pointsPossible'); + BuiltValueNullFieldError.checkNotNull( + courseId, r'CreateAssignmentInfo', 'courseId'); + BuiltValueNullFieldError.checkNotNull( + published, r'CreateAssignmentInfo', 'published'); } @override @@ -404,49 +377,36 @@ class _$CreateAssignmentInfo extends CreateAssignmentInfo { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc($jc(0, name.hashCode), description.hashCode), dueAt.hashCode), - pointsPossible.hashCode), - courseId.hashCode), - gradingType.hashCode), - htmlUrl.hashCode), - url.hashCode), - quizId.hashCode), - useRubricForGrading.hashCode), - assignmentGroupId.hashCode), - position.hashCode), - lockInfo.hashCode), - lockedForUser.hashCode), - lockAt.hashCode), - unlockAt.hashCode), - lockExplanation.hashCode), - freeFormCriterionComments.hashCode), - published.hashCode), - muted.hashCode), - groupCategoryId.hashCode), - submissionTypes.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, dueAt.hashCode); + _$hash = $jc(_$hash, pointsPossible.hashCode); + _$hash = $jc(_$hash, courseId.hashCode); + _$hash = $jc(_$hash, gradingType.hashCode); + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, quizId.hashCode); + _$hash = $jc(_$hash, useRubricForGrading.hashCode); + _$hash = $jc(_$hash, assignmentGroupId.hashCode); + _$hash = $jc(_$hash, position.hashCode); + _$hash = $jc(_$hash, lockInfo.hashCode); + _$hash = $jc(_$hash, lockedForUser.hashCode); + _$hash = $jc(_$hash, lockAt.hashCode); + _$hash = $jc(_$hash, unlockAt.hashCode); + _$hash = $jc(_$hash, lockExplanation.hashCode); + _$hash = $jc(_$hash, freeFormCriterionComments.hashCode); + _$hash = $jc(_$hash, published.hashCode); + _$hash = $jc(_$hash, muted.hashCode); + _$hash = $jc(_$hash, groupCategoryId.hashCode); + _$hash = $jc(_$hash, submissionTypes.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateAssignmentInfo') + return (newBuiltValueToStringHelper(r'CreateAssignmentInfo') ..add('name', name) ..add('description', description) ..add('dueAt', dueAt) @@ -475,103 +435,104 @@ class _$CreateAssignmentInfo extends CreateAssignmentInfo { class CreateAssignmentInfoBuilder implements Builder { - _$CreateAssignmentInfo _$v; + _$CreateAssignmentInfo? _$v; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _description; - String get description => _$this._description; - set description(String description) => _$this._description = description; + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; - DateTime _dueAt; - DateTime get dueAt => _$this._dueAt; - set dueAt(DateTime dueAt) => _$this._dueAt = dueAt; + DateTime? _dueAt; + DateTime? get dueAt => _$this._dueAt; + set dueAt(DateTime? dueAt) => _$this._dueAt = dueAt; - double _pointsPossible; - double get pointsPossible => _$this._pointsPossible; - set pointsPossible(double pointsPossible) => + double? _pointsPossible; + double? get pointsPossible => _$this._pointsPossible; + set pointsPossible(double? pointsPossible) => _$this._pointsPossible = pointsPossible; - String _courseId; - String get courseId => _$this._courseId; - set courseId(String courseId) => _$this._courseId = courseId; + String? _courseId; + String? get courseId => _$this._courseId; + set courseId(String? courseId) => _$this._courseId = courseId; - GradingType _gradingType; - GradingType get gradingType => _$this._gradingType; - set gradingType(GradingType gradingType) => _$this._gradingType = gradingType; + GradingType? _gradingType; + GradingType? get gradingType => _$this._gradingType; + set gradingType(GradingType? gradingType) => + _$this._gradingType = gradingType; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - String _quizId; - String get quizId => _$this._quizId; - set quizId(String quizId) => _$this._quizId = quizId; + String? _quizId; + String? get quizId => _$this._quizId; + set quizId(String? quizId) => _$this._quizId = quizId; - bool _useRubricForGrading; - bool get useRubricForGrading => _$this._useRubricForGrading; - set useRubricForGrading(bool useRubricForGrading) => + bool? _useRubricForGrading; + bool? get useRubricForGrading => _$this._useRubricForGrading; + set useRubricForGrading(bool? useRubricForGrading) => _$this._useRubricForGrading = useRubricForGrading; - String _assignmentGroupId; - String get assignmentGroupId => _$this._assignmentGroupId; - set assignmentGroupId(String assignmentGroupId) => + String? _assignmentGroupId; + String? get assignmentGroupId => _$this._assignmentGroupId; + set assignmentGroupId(String? assignmentGroupId) => _$this._assignmentGroupId = assignmentGroupId; - int _position; - int get position => _$this._position; - set position(int position) => _$this._position = position; + int? _position; + int? get position => _$this._position; + set position(int? position) => _$this._position = position; - LockInfoBuilder _lockInfo; + LockInfoBuilder? _lockInfo; LockInfoBuilder get lockInfo => _$this._lockInfo ??= new LockInfoBuilder(); - set lockInfo(LockInfoBuilder lockInfo) => _$this._lockInfo = lockInfo; + set lockInfo(LockInfoBuilder? lockInfo) => _$this._lockInfo = lockInfo; - bool _lockedForUser; - bool get lockedForUser => _$this._lockedForUser; - set lockedForUser(bool lockedForUser) => + bool? _lockedForUser; + bool? get lockedForUser => _$this._lockedForUser; + set lockedForUser(bool? lockedForUser) => _$this._lockedForUser = lockedForUser; - DateTime _lockAt; - DateTime get lockAt => _$this._lockAt; - set lockAt(DateTime lockAt) => _$this._lockAt = lockAt; + DateTime? _lockAt; + DateTime? get lockAt => _$this._lockAt; + set lockAt(DateTime? lockAt) => _$this._lockAt = lockAt; - DateTime _unlockAt; - DateTime get unlockAt => _$this._unlockAt; - set unlockAt(DateTime unlockAt) => _$this._unlockAt = unlockAt; + DateTime? _unlockAt; + DateTime? get unlockAt => _$this._unlockAt; + set unlockAt(DateTime? unlockAt) => _$this._unlockAt = unlockAt; - String _lockExplanation; - String get lockExplanation => _$this._lockExplanation; - set lockExplanation(String lockExplanation) => + String? _lockExplanation; + String? get lockExplanation => _$this._lockExplanation; + set lockExplanation(String? lockExplanation) => _$this._lockExplanation = lockExplanation; - bool _freeFormCriterionComments; - bool get freeFormCriterionComments => _$this._freeFormCriterionComments; - set freeFormCriterionComments(bool freeFormCriterionComments) => + bool? _freeFormCriterionComments; + bool? get freeFormCriterionComments => _$this._freeFormCriterionComments; + set freeFormCriterionComments(bool? freeFormCriterionComments) => _$this._freeFormCriterionComments = freeFormCriterionComments; - bool _published; - bool get published => _$this._published; - set published(bool published) => _$this._published = published; + bool? _published; + bool? get published => _$this._published; + set published(bool? published) => _$this._published = published; - bool _muted; - bool get muted => _$this._muted; - set muted(bool muted) => _$this._muted = muted; + bool? _muted; + bool? get muted => _$this._muted; + set muted(bool? muted) => _$this._muted = muted; - String _groupCategoryId; - String get groupCategoryId => _$this._groupCategoryId; - set groupCategoryId(String groupCategoryId) => + String? _groupCategoryId; + String? get groupCategoryId => _$this._groupCategoryId; + set groupCategoryId(String? groupCategoryId) => _$this._groupCategoryId = groupCategoryId; - ListBuilder _submissionTypes; + ListBuilder? _submissionTypes; ListBuilder get submissionTypes => _$this._submissionTypes ??= new ListBuilder(); - set submissionTypes(ListBuilder submissionTypes) => + set submissionTypes(ListBuilder? submissionTypes) => _$this._submissionTypes = submissionTypes; CreateAssignmentInfoBuilder() { @@ -579,29 +540,30 @@ class CreateAssignmentInfoBuilder } CreateAssignmentInfoBuilder get _$this { - if (_$v != null) { - _name = _$v.name; - _description = _$v.description; - _dueAt = _$v.dueAt; - _pointsPossible = _$v.pointsPossible; - _courseId = _$v.courseId; - _gradingType = _$v.gradingType; - _htmlUrl = _$v.htmlUrl; - _url = _$v.url; - _quizId = _$v.quizId; - _useRubricForGrading = _$v.useRubricForGrading; - _assignmentGroupId = _$v.assignmentGroupId; - _position = _$v.position; - _lockInfo = _$v.lockInfo?.toBuilder(); - _lockedForUser = _$v.lockedForUser; - _lockAt = _$v.lockAt; - _unlockAt = _$v.unlockAt; - _lockExplanation = _$v.lockExplanation; - _freeFormCriterionComments = _$v.freeFormCriterionComments; - _published = _$v.published; - _muted = _$v.muted; - _groupCategoryId = _$v.groupCategoryId; - _submissionTypes = _$v.submissionTypes?.toBuilder(); + final $v = _$v; + if ($v != null) { + _name = $v.name; + _description = $v.description; + _dueAt = $v.dueAt; + _pointsPossible = $v.pointsPossible; + _courseId = $v.courseId; + _gradingType = $v.gradingType; + _htmlUrl = $v.htmlUrl; + _url = $v.url; + _quizId = $v.quizId; + _useRubricForGrading = $v.useRubricForGrading; + _assignmentGroupId = $v.assignmentGroupId; + _position = $v.position; + _lockInfo = $v.lockInfo?.toBuilder(); + _lockedForUser = $v.lockedForUser; + _lockAt = $v.lockAt; + _unlockAt = $v.unlockAt; + _lockExplanation = $v.lockExplanation; + _freeFormCriterionComments = $v.freeFormCriterionComments; + _published = $v.published; + _muted = $v.muted; + _groupCategoryId = $v.groupCategoryId; + _submissionTypes = $v.submissionTypes?.toBuilder(); _$v = null; } return this; @@ -609,28 +571,31 @@ class CreateAssignmentInfoBuilder @override void replace(CreateAssignmentInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateAssignmentInfo; } @override - void update(void Function(CreateAssignmentInfoBuilder) updates) { + void update(void Function(CreateAssignmentInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateAssignmentInfo build() { + CreateAssignmentInfo build() => _build(); + + _$CreateAssignmentInfo _build() { _$CreateAssignmentInfo _$result; try { _$result = _$v ?? new _$CreateAssignmentInfo._( - name: name, + name: BuiltValueNullFieldError.checkNotNull( + name, r'CreateAssignmentInfo', 'name'), description: description, dueAt: dueAt, - pointsPossible: pointsPossible, - courseId: courseId, + pointsPossible: BuiltValueNullFieldError.checkNotNull( + pointsPossible, r'CreateAssignmentInfo', 'pointsPossible'), + courseId: BuiltValueNullFieldError.checkNotNull( + courseId, r'CreateAssignmentInfo', 'courseId'), gradingType: gradingType, htmlUrl: htmlUrl, url: url, @@ -644,12 +609,13 @@ class CreateAssignmentInfoBuilder unlockAt: unlockAt, lockExplanation: lockExplanation, freeFormCriterionComments: freeFormCriterionComments, - published: published, + published: BuiltValueNullFieldError.checkNotNull( + published, r'CreateAssignmentInfo', 'published'), muted: muted, groupCategoryId: groupCategoryId, submissionTypes: _submissionTypes?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'lockInfo'; _lockInfo?.build(); @@ -658,7 +624,7 @@ class CreateAssignmentInfoBuilder _submissionTypes?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateAssignmentInfo', _$failedField, e.toString()); + r'CreateAssignmentInfo', _$failedField, e.toString()); } rethrow; } @@ -667,4 +633,4 @@ class CreateAssignmentInfoBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_assignment_wrapper.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_assignment_wrapper.g.dart index a5c63170ac..92ec7895dd 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_assignment_wrapper.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_assignment_wrapper.g.dart @@ -20,10 +20,10 @@ class _$CreateAssignmentWrapperSerializer final String wireName = 'CreateAssignmentWrapper'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateAssignmentWrapper object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'assignment', serializers.serialize(object.assignment, specifiedType: const FullType(CreateAssignmentInfo)), @@ -34,20 +34,19 @@ class _$CreateAssignmentWrapperSerializer @override CreateAssignmentWrapper deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateAssignmentWrapperBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'assignment': result.assignment.replace(serializers.deserialize(value, - specifiedType: const FullType(CreateAssignmentInfo)) + specifiedType: const FullType(CreateAssignmentInfo))! as CreateAssignmentInfo); break; } @@ -62,14 +61,12 @@ class _$CreateAssignmentWrapper extends CreateAssignmentWrapper { final CreateAssignmentInfo assignment; factory _$CreateAssignmentWrapper( - [void Function(CreateAssignmentWrapperBuilder) updates]) => - (new CreateAssignmentWrapperBuilder()..update(updates)).build(); + [void Function(CreateAssignmentWrapperBuilder)? updates]) => + (new CreateAssignmentWrapperBuilder()..update(updates))._build(); - _$CreateAssignmentWrapper._({this.assignment}) : super._() { - if (assignment == null) { - throw new BuiltValueNullFieldError( - 'CreateAssignmentWrapper', 'assignment'); - } + _$CreateAssignmentWrapper._({required this.assignment}) : super._() { + BuiltValueNullFieldError.checkNotNull( + assignment, r'CreateAssignmentWrapper', 'assignment'); } @override @@ -89,12 +86,15 @@ class _$CreateAssignmentWrapper extends CreateAssignmentWrapper { @override int get hashCode { - return $jf($jc(0, assignment.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, assignment.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateAssignmentWrapper') + return (newBuiltValueToStringHelper(r'CreateAssignmentWrapper') ..add('assignment', assignment)) .toString(); } @@ -103,12 +103,12 @@ class _$CreateAssignmentWrapper extends CreateAssignmentWrapper { class CreateAssignmentWrapperBuilder implements Builder { - _$CreateAssignmentWrapper _$v; + _$CreateAssignmentWrapper? _$v; - CreateAssignmentInfoBuilder _assignment; + CreateAssignmentInfoBuilder? _assignment; CreateAssignmentInfoBuilder get assignment => _$this._assignment ??= new CreateAssignmentInfoBuilder(); - set assignment(CreateAssignmentInfoBuilder assignment) => + set assignment(CreateAssignmentInfoBuilder? assignment) => _$this._assignment = assignment; CreateAssignmentWrapperBuilder() { @@ -116,8 +116,9 @@ class CreateAssignmentWrapperBuilder } CreateAssignmentWrapperBuilder get _$this { - if (_$v != null) { - _assignment = _$v.assignment?.toBuilder(); + final $v = _$v; + if ($v != null) { + _assignment = $v.assignment.toBuilder(); _$v = null; } return this; @@ -125,31 +126,31 @@ class CreateAssignmentWrapperBuilder @override void replace(CreateAssignmentWrapper other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateAssignmentWrapper; } @override - void update(void Function(CreateAssignmentWrapperBuilder) updates) { + void update(void Function(CreateAssignmentWrapperBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateAssignmentWrapper build() { + CreateAssignmentWrapper build() => _build(); + + _$CreateAssignmentWrapper _build() { _$CreateAssignmentWrapper _$result; try { _$result = _$v ?? new _$CreateAssignmentWrapper._(assignment: assignment.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'assignment'; assignment.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateAssignmentWrapper', _$failedField, e.toString()); + r'CreateAssignmentWrapper', _$failedField, e.toString()); } rethrow; } @@ -158,4 +159,4 @@ class CreateAssignmentWrapperBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_course_info.dart b/apps/flutter_parent/lib/models/dataseeding/create_course_info.dart index bcd61c243b..e9a88a6bb1 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_course_info.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_course_info.dart @@ -25,15 +25,17 @@ abstract class CreateCourseInfo implements Built b..role = "student"; } diff --git a/apps/flutter_parent/lib/models/dataseeding/create_course_info.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_course_info.g.dart index 8f9cc02eab..c20a27641b 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_course_info.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_course_info.g.dart @@ -17,9 +17,9 @@ class _$CreateCourseInfoSerializer final String wireName = 'CreateCourseInfo'; @override - Iterable serialize(Serializers serializers, CreateCourseInfo object, + Iterable serialize(Serializers serializers, CreateCourseInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), 'course_code', @@ -28,55 +28,53 @@ class _$CreateCourseInfoSerializer 'role', serializers.serialize(object.role, specifiedType: const FullType(String)), ]; - result.add('enrollment_term_id'); - if (object.enrollmentTermId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.enrollmentTermId, - specifiedType: const FullType(int))); - } - result.add('syllabus_body'); - if (object.syllabusBody == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.syllabusBody, - specifiedType: const FullType(String))); - } + Object? value; + value = object.enrollmentTermId; + + result + ..add('enrollment_term_id') + ..add(serializers.serialize(value, specifiedType: const FullType(int))); + value = object.syllabusBody; + + result + ..add('syllabus_body') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override CreateCourseInfo deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateCourseInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'course_code': result.courseCode = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'enrollment_term_id': result.enrollmentTermId = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int)) as int?; break; case 'role': result.role = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'syllabus_body': result.syllabusBody = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -91,32 +89,27 @@ class _$CreateCourseInfo extends CreateCourseInfo { @override final String courseCode; @override - final int enrollmentTermId; + final int? enrollmentTermId; @override final String role; @override - final String syllabusBody; + final String? syllabusBody; factory _$CreateCourseInfo( - [void Function(CreateCourseInfoBuilder) updates]) => - (new CreateCourseInfoBuilder()..update(updates)).build(); + [void Function(CreateCourseInfoBuilder)? updates]) => + (new CreateCourseInfoBuilder()..update(updates))._build(); _$CreateCourseInfo._( - {this.name, - this.courseCode, + {required this.name, + required this.courseCode, this.enrollmentTermId, - this.role, + required this.role, this.syllabusBody}) : super._() { - if (name == null) { - throw new BuiltValueNullFieldError('CreateCourseInfo', 'name'); - } - if (courseCode == null) { - throw new BuiltValueNullFieldError('CreateCourseInfo', 'courseCode'); - } - if (role == null) { - throw new BuiltValueNullFieldError('CreateCourseInfo', 'role'); - } + BuiltValueNullFieldError.checkNotNull(name, r'CreateCourseInfo', 'name'); + BuiltValueNullFieldError.checkNotNull( + courseCode, r'CreateCourseInfo', 'courseCode'); + BuiltValueNullFieldError.checkNotNull(role, r'CreateCourseInfo', 'role'); } @override @@ -140,17 +133,19 @@ class _$CreateCourseInfo extends CreateCourseInfo { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc(0, name.hashCode), courseCode.hashCode), - enrollmentTermId.hashCode), - role.hashCode), - syllabusBody.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, courseCode.hashCode); + _$hash = $jc(_$hash, enrollmentTermId.hashCode); + _$hash = $jc(_$hash, role.hashCode); + _$hash = $jc(_$hash, syllabusBody.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateCourseInfo') + return (newBuiltValueToStringHelper(r'CreateCourseInfo') ..add('name', name) ..add('courseCode', courseCode) ..add('enrollmentTermId', enrollmentTermId) @@ -162,40 +157,41 @@ class _$CreateCourseInfo extends CreateCourseInfo { class CreateCourseInfoBuilder implements Builder { - _$CreateCourseInfo _$v; + _$CreateCourseInfo? _$v; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _courseCode; - String get courseCode => _$this._courseCode; - set courseCode(String courseCode) => _$this._courseCode = courseCode; + String? _courseCode; + String? get courseCode => _$this._courseCode; + set courseCode(String? courseCode) => _$this._courseCode = courseCode; - int _enrollmentTermId; - int get enrollmentTermId => _$this._enrollmentTermId; - set enrollmentTermId(int enrollmentTermId) => + int? _enrollmentTermId; + int? get enrollmentTermId => _$this._enrollmentTermId; + set enrollmentTermId(int? enrollmentTermId) => _$this._enrollmentTermId = enrollmentTermId; - String _role; - String get role => _$this._role; - set role(String role) => _$this._role = role; + String? _role; + String? get role => _$this._role; + set role(String? role) => _$this._role = role; - String _syllabusBody; - String get syllabusBody => _$this._syllabusBody; - set syllabusBody(String syllabusBody) => _$this._syllabusBody = syllabusBody; + String? _syllabusBody; + String? get syllabusBody => _$this._syllabusBody; + set syllabusBody(String? syllabusBody) => _$this._syllabusBody = syllabusBody; CreateCourseInfoBuilder() { CreateCourseInfo._initializeBuilder(this); } CreateCourseInfoBuilder get _$this { - if (_$v != null) { - _name = _$v.name; - _courseCode = _$v.courseCode; - _enrollmentTermId = _$v.enrollmentTermId; - _role = _$v.role; - _syllabusBody = _$v.syllabusBody; + final $v = _$v; + if ($v != null) { + _name = $v.name; + _courseCode = $v.courseCode; + _enrollmentTermId = $v.enrollmentTermId; + _role = $v.role; + _syllabusBody = $v.syllabusBody; _$v = null; } return this; @@ -203,29 +199,32 @@ class CreateCourseInfoBuilder @override void replace(CreateCourseInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateCourseInfo; } @override - void update(void Function(CreateCourseInfoBuilder) updates) { + void update(void Function(CreateCourseInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateCourseInfo build() { + CreateCourseInfo build() => _build(); + + _$CreateCourseInfo _build() { final _$result = _$v ?? new _$CreateCourseInfo._( - name: name, - courseCode: courseCode, + name: BuiltValueNullFieldError.checkNotNull( + name, r'CreateCourseInfo', 'name'), + courseCode: BuiltValueNullFieldError.checkNotNull( + courseCode, r'CreateCourseInfo', 'courseCode'), enrollmentTermId: enrollmentTermId, - role: role, + role: BuiltValueNullFieldError.checkNotNull( + role, r'CreateCourseInfo', 'role'), syllabusBody: syllabusBody); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_course_wrapper.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_course_wrapper.g.dart index 35f3e1346b..5fa72345bc 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_course_wrapper.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_course_wrapper.g.dart @@ -20,10 +20,10 @@ class _$CreateCourseWrapperSerializer final String wireName = 'CreateCourseWrapper'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateCourseWrapper object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'course', serializers.serialize(object.course, specifiedType: const FullType(CreateCourseInfo)), @@ -36,25 +36,24 @@ class _$CreateCourseWrapperSerializer @override CreateCourseWrapper deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateCourseWrapperBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'course': result.course.replace(serializers.deserialize(value, - specifiedType: const FullType(CreateCourseInfo)) + specifiedType: const FullType(CreateCourseInfo))! as CreateCourseInfo); break; case 'offer': result.offer = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -70,16 +69,15 @@ class _$CreateCourseWrapper extends CreateCourseWrapper { final bool offer; factory _$CreateCourseWrapper( - [void Function(CreateCourseWrapperBuilder) updates]) => - (new CreateCourseWrapperBuilder()..update(updates)).build(); - - _$CreateCourseWrapper._({this.course, this.offer}) : super._() { - if (course == null) { - throw new BuiltValueNullFieldError('CreateCourseWrapper', 'course'); - } - if (offer == null) { - throw new BuiltValueNullFieldError('CreateCourseWrapper', 'offer'); - } + [void Function(CreateCourseWrapperBuilder)? updates]) => + (new CreateCourseWrapperBuilder()..update(updates))._build(); + + _$CreateCourseWrapper._({required this.course, required this.offer}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + course, r'CreateCourseWrapper', 'course'); + BuiltValueNullFieldError.checkNotNull( + offer, r'CreateCourseWrapper', 'offer'); } @override @@ -101,12 +99,16 @@ class _$CreateCourseWrapper extends CreateCourseWrapper { @override int get hashCode { - return $jf($jc($jc(0, course.hashCode), offer.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, course.hashCode); + _$hash = $jc(_$hash, offer.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateCourseWrapper') + return (newBuiltValueToStringHelper(r'CreateCourseWrapper') ..add('course', course) ..add('offer', offer)) .toString(); @@ -115,25 +117,26 @@ class _$CreateCourseWrapper extends CreateCourseWrapper { class CreateCourseWrapperBuilder implements Builder { - _$CreateCourseWrapper _$v; + _$CreateCourseWrapper? _$v; - CreateCourseInfoBuilder _course; + CreateCourseInfoBuilder? _course; CreateCourseInfoBuilder get course => _$this._course ??= new CreateCourseInfoBuilder(); - set course(CreateCourseInfoBuilder course) => _$this._course = course; + set course(CreateCourseInfoBuilder? course) => _$this._course = course; - bool _offer; - bool get offer => _$this._offer; - set offer(bool offer) => _$this._offer = offer; + bool? _offer; + bool? get offer => _$this._offer; + set offer(bool? offer) => _$this._offer = offer; CreateCourseWrapperBuilder() { CreateCourseWrapper._initializeBuilder(this); } CreateCourseWrapperBuilder get _$this { - if (_$v != null) { - _course = _$v.course?.toBuilder(); - _offer = _$v.offer; + final $v = _$v; + if ($v != null) { + _course = $v.course.toBuilder(); + _offer = $v.offer; _$v = null; } return this; @@ -141,31 +144,34 @@ class CreateCourseWrapperBuilder @override void replace(CreateCourseWrapper other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateCourseWrapper; } @override - void update(void Function(CreateCourseWrapperBuilder) updates) { + void update(void Function(CreateCourseWrapperBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateCourseWrapper build() { + CreateCourseWrapper build() => _build(); + + _$CreateCourseWrapper _build() { _$CreateCourseWrapper _$result; try { _$result = _$v ?? - new _$CreateCourseWrapper._(course: course.build(), offer: offer); + new _$CreateCourseWrapper._( + course: course.build(), + offer: BuiltValueNullFieldError.checkNotNull( + offer, r'CreateCourseWrapper', 'offer')); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'course'; course.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateCourseWrapper', _$failedField, e.toString()); + r'CreateCourseWrapper', _$failedField, e.toString()); } rethrow; } @@ -174,4 +180,4 @@ class CreateCourseWrapperBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.dart b/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.dart index 15cba1eb67..7c92c7df34 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.dart @@ -26,13 +26,16 @@ abstract class CreateEnrollmentInfo implements Built b..role = ""; } diff --git a/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.g.dart index 2494588457..2de6540dd6 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_enrollment_info.g.dart @@ -20,10 +20,10 @@ class _$CreateEnrollmentInfoSerializer final String wireName = 'CreateEnrollmentInfo'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateEnrollmentInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'user_id', serializers.serialize(object.userId, specifiedType: const FullType(String)), @@ -35,48 +35,48 @@ class _$CreateEnrollmentInfoSerializer serializers.serialize(object.enrollmentState, specifiedType: const FullType(String)), ]; - result.add('associated_user_id'); - if (object.associatedUserId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.associatedUserId, - specifiedType: const FullType(String))); - } + Object? value; + value = object.associatedUserId; + + result + ..add('associated_user_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override CreateEnrollmentInfo deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateEnrollmentInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'role': result.role = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'enrollment_state': result.enrollmentState = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'associated_user_id': result.associatedUserId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -95,32 +95,27 @@ class _$CreateEnrollmentInfo extends CreateEnrollmentInfo { @override final String enrollmentState; @override - final String associatedUserId; + final String? associatedUserId; factory _$CreateEnrollmentInfo( - [void Function(CreateEnrollmentInfoBuilder) updates]) => - (new CreateEnrollmentInfoBuilder()..update(updates)).build(); + [void Function(CreateEnrollmentInfoBuilder)? updates]) => + (new CreateEnrollmentInfoBuilder()..update(updates))._build(); _$CreateEnrollmentInfo._( - {this.userId, - this.type, - this.role, - this.enrollmentState, + {required this.userId, + required this.type, + required this.role, + required this.enrollmentState, this.associatedUserId}) : super._() { - if (userId == null) { - throw new BuiltValueNullFieldError('CreateEnrollmentInfo', 'userId'); - } - if (type == null) { - throw new BuiltValueNullFieldError('CreateEnrollmentInfo', 'type'); - } - if (role == null) { - throw new BuiltValueNullFieldError('CreateEnrollmentInfo', 'role'); - } - if (enrollmentState == null) { - throw new BuiltValueNullFieldError( - 'CreateEnrollmentInfo', 'enrollmentState'); - } + BuiltValueNullFieldError.checkNotNull( + userId, r'CreateEnrollmentInfo', 'userId'); + BuiltValueNullFieldError.checkNotNull( + type, r'CreateEnrollmentInfo', 'type'); + BuiltValueNullFieldError.checkNotNull( + role, r'CreateEnrollmentInfo', 'role'); + BuiltValueNullFieldError.checkNotNull( + enrollmentState, r'CreateEnrollmentInfo', 'enrollmentState'); } @override @@ -145,15 +140,19 @@ class _$CreateEnrollmentInfo extends CreateEnrollmentInfo { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, userId.hashCode), type.hashCode), role.hashCode), - enrollmentState.hashCode), - associatedUserId.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, role.hashCode); + _$hash = $jc(_$hash, enrollmentState.hashCode); + _$hash = $jc(_$hash, associatedUserId.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateEnrollmentInfo') + return (newBuiltValueToStringHelper(r'CreateEnrollmentInfo') ..add('userId', userId) ..add('type', type) ..add('role', role) @@ -165,28 +164,28 @@ class _$CreateEnrollmentInfo extends CreateEnrollmentInfo { class CreateEnrollmentInfoBuilder implements Builder { - _$CreateEnrollmentInfo _$v; + _$CreateEnrollmentInfo? _$v; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _type; - String get type => _$this._type; - set type(String type) => _$this._type = type; + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; - String _role; - String get role => _$this._role; - set role(String role) => _$this._role = role; + String? _role; + String? get role => _$this._role; + set role(String? role) => _$this._role = role; - String _enrollmentState; - String get enrollmentState => _$this._enrollmentState; - set enrollmentState(String enrollmentState) => + String? _enrollmentState; + String? get enrollmentState => _$this._enrollmentState; + set enrollmentState(String? enrollmentState) => _$this._enrollmentState = enrollmentState; - String _associatedUserId; - String get associatedUserId => _$this._associatedUserId; - set associatedUserId(String associatedUserId) => + String? _associatedUserId; + String? get associatedUserId => _$this._associatedUserId; + set associatedUserId(String? associatedUserId) => _$this._associatedUserId = associatedUserId; CreateEnrollmentInfoBuilder() { @@ -194,12 +193,13 @@ class CreateEnrollmentInfoBuilder } CreateEnrollmentInfoBuilder get _$this { - if (_$v != null) { - _userId = _$v.userId; - _type = _$v.type; - _role = _$v.role; - _enrollmentState = _$v.enrollmentState; - _associatedUserId = _$v.associatedUserId; + final $v = _$v; + if ($v != null) { + _userId = $v.userId; + _type = $v.type; + _role = $v.role; + _enrollmentState = $v.enrollmentState; + _associatedUserId = $v.associatedUserId; _$v = null; } return this; @@ -207,29 +207,33 @@ class CreateEnrollmentInfoBuilder @override void replace(CreateEnrollmentInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateEnrollmentInfo; } @override - void update(void Function(CreateEnrollmentInfoBuilder) updates) { + void update(void Function(CreateEnrollmentInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateEnrollmentInfo build() { + CreateEnrollmentInfo build() => _build(); + + _$CreateEnrollmentInfo _build() { final _$result = _$v ?? new _$CreateEnrollmentInfo._( - userId: userId, - type: type, - role: role, - enrollmentState: enrollmentState, + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'CreateEnrollmentInfo', 'userId'), + type: BuiltValueNullFieldError.checkNotNull( + type, r'CreateEnrollmentInfo', 'type'), + role: BuiltValueNullFieldError.checkNotNull( + role, r'CreateEnrollmentInfo', 'role'), + enrollmentState: BuiltValueNullFieldError.checkNotNull( + enrollmentState, r'CreateEnrollmentInfo', 'enrollmentState'), associatedUserId: associatedUserId); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_enrollment_wrapper.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_enrollment_wrapper.g.dart index 7a83143734..63c5691d77 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_enrollment_wrapper.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_enrollment_wrapper.g.dart @@ -20,10 +20,10 @@ class _$CreateEnrollmentWrapperSerializer final String wireName = 'CreateEnrollmentWrapper'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateEnrollmentWrapper object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'enrollment', serializers.serialize(object.enrollment, specifiedType: const FullType(CreateEnrollmentInfo)), @@ -34,20 +34,19 @@ class _$CreateEnrollmentWrapperSerializer @override CreateEnrollmentWrapper deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateEnrollmentWrapperBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'enrollment': result.enrollment.replace(serializers.deserialize(value, - specifiedType: const FullType(CreateEnrollmentInfo)) + specifiedType: const FullType(CreateEnrollmentInfo))! as CreateEnrollmentInfo); break; } @@ -62,14 +61,12 @@ class _$CreateEnrollmentWrapper extends CreateEnrollmentWrapper { final CreateEnrollmentInfo enrollment; factory _$CreateEnrollmentWrapper( - [void Function(CreateEnrollmentWrapperBuilder) updates]) => - (new CreateEnrollmentWrapperBuilder()..update(updates)).build(); + [void Function(CreateEnrollmentWrapperBuilder)? updates]) => + (new CreateEnrollmentWrapperBuilder()..update(updates))._build(); - _$CreateEnrollmentWrapper._({this.enrollment}) : super._() { - if (enrollment == null) { - throw new BuiltValueNullFieldError( - 'CreateEnrollmentWrapper', 'enrollment'); - } + _$CreateEnrollmentWrapper._({required this.enrollment}) : super._() { + BuiltValueNullFieldError.checkNotNull( + enrollment, r'CreateEnrollmentWrapper', 'enrollment'); } @override @@ -89,12 +86,15 @@ class _$CreateEnrollmentWrapper extends CreateEnrollmentWrapper { @override int get hashCode { - return $jf($jc(0, enrollment.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, enrollment.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateEnrollmentWrapper') + return (newBuiltValueToStringHelper(r'CreateEnrollmentWrapper') ..add('enrollment', enrollment)) .toString(); } @@ -103,12 +103,12 @@ class _$CreateEnrollmentWrapper extends CreateEnrollmentWrapper { class CreateEnrollmentWrapperBuilder implements Builder { - _$CreateEnrollmentWrapper _$v; + _$CreateEnrollmentWrapper? _$v; - CreateEnrollmentInfoBuilder _enrollment; + CreateEnrollmentInfoBuilder? _enrollment; CreateEnrollmentInfoBuilder get enrollment => _$this._enrollment ??= new CreateEnrollmentInfoBuilder(); - set enrollment(CreateEnrollmentInfoBuilder enrollment) => + set enrollment(CreateEnrollmentInfoBuilder? enrollment) => _$this._enrollment = enrollment; CreateEnrollmentWrapperBuilder() { @@ -116,8 +116,9 @@ class CreateEnrollmentWrapperBuilder } CreateEnrollmentWrapperBuilder get _$this { - if (_$v != null) { - _enrollment = _$v.enrollment?.toBuilder(); + final $v = _$v; + if ($v != null) { + _enrollment = $v.enrollment.toBuilder(); _$v = null; } return this; @@ -125,31 +126,31 @@ class CreateEnrollmentWrapperBuilder @override void replace(CreateEnrollmentWrapper other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateEnrollmentWrapper; } @override - void update(void Function(CreateEnrollmentWrapperBuilder) updates) { + void update(void Function(CreateEnrollmentWrapperBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateEnrollmentWrapper build() { + CreateEnrollmentWrapper build() => _build(); + + _$CreateEnrollmentWrapper _build() { _$CreateEnrollmentWrapper _$result; try { _$result = _$v ?? new _$CreateEnrollmentWrapper._(enrollment: enrollment.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'enrollment'; enrollment.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateEnrollmentWrapper', _$failedField, e.toString()); + r'CreateEnrollmentWrapper', _$failedField, e.toString()); } rethrow; } @@ -158,4 +159,4 @@ class CreateEnrollmentWrapperBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_submission_info.dart b/apps/flutter_parent/lib/models/dataseeding/create_submission_info.dart index efa91949db..74eb58138f 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_submission_info.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_submission_info.dart @@ -25,26 +25,21 @@ abstract class CreateSubmissionInfo implements Built b; } diff --git a/apps/flutter_parent/lib/models/dataseeding/create_submission_info.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_submission_info.g.dart index 8e601af34c..a0362d6953 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_submission_info.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_submission_info.g.dart @@ -20,80 +20,75 @@ class _$CreateSubmissionInfoSerializer final String wireName = 'CreateSubmissionInfo'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateSubmissionInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - result.add('submission_type'); - if (object.submissionType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionType, - specifiedType: const FullType(String))); - } - result.add('body'); - if (object.body == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.body, - specifiedType: const FullType(String))); - } - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('user_id'); - if (object.userId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.userId, - specifiedType: const FullType(int))); - } - result.add('submitted_at'); - if (object.submittedAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submittedAt, + final result = []; + Object? value; + value = object.submissionType; + + result + ..add('submission_type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.body; + + result + ..add('body') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.userId; + + result + ..add('user_id') + ..add(serializers.serialize(value, specifiedType: const FullType(int))); + value = object.submittedAt; + + result + ..add('submitted_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override CreateSubmissionInfo deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateSubmissionInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'submission_type': result.submissionType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'body': result.body = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int)) as int?; break; case 'submitted_at': result.submittedAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; } } @@ -104,19 +99,19 @@ class _$CreateSubmissionInfoSerializer class _$CreateSubmissionInfo extends CreateSubmissionInfo { @override - final String submissionType; + final String? submissionType; @override - final String body; + final String? body; @override - final String url; + final String? url; @override - final int userId; + final int? userId; @override - final DateTime submittedAt; + final DateTime? submittedAt; factory _$CreateSubmissionInfo( - [void Function(CreateSubmissionInfoBuilder) updates]) => - (new CreateSubmissionInfoBuilder()..update(updates)).build(); + [void Function(CreateSubmissionInfoBuilder)? updates]) => + (new CreateSubmissionInfoBuilder()..update(updates))._build(); _$CreateSubmissionInfo._( {this.submissionType, this.body, this.url, this.userId, this.submittedAt}) @@ -144,17 +139,19 @@ class _$CreateSubmissionInfo extends CreateSubmissionInfo { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc(0, submissionType.hashCode), body.hashCode), - url.hashCode), - userId.hashCode), - submittedAt.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, submissionType.hashCode); + _$hash = $jc(_$hash, body.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, submittedAt.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateSubmissionInfo') + return (newBuiltValueToStringHelper(r'CreateSubmissionInfo') ..add('submissionType', submissionType) ..add('body', body) ..add('url', url) @@ -166,40 +163,41 @@ class _$CreateSubmissionInfo extends CreateSubmissionInfo { class CreateSubmissionInfoBuilder implements Builder { - _$CreateSubmissionInfo _$v; + _$CreateSubmissionInfo? _$v; - String _submissionType; - String get submissionType => _$this._submissionType; - set submissionType(String submissionType) => + String? _submissionType; + String? get submissionType => _$this._submissionType; + set submissionType(String? submissionType) => _$this._submissionType = submissionType; - String _body; - String get body => _$this._body; - set body(String body) => _$this._body = body; + String? _body; + String? get body => _$this._body; + set body(String? body) => _$this._body = body; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - int _userId; - int get userId => _$this._userId; - set userId(int userId) => _$this._userId = userId; + int? _userId; + int? get userId => _$this._userId; + set userId(int? userId) => _$this._userId = userId; - DateTime _submittedAt; - DateTime get submittedAt => _$this._submittedAt; - set submittedAt(DateTime submittedAt) => _$this._submittedAt = submittedAt; + DateTime? _submittedAt; + DateTime? get submittedAt => _$this._submittedAt; + set submittedAt(DateTime? submittedAt) => _$this._submittedAt = submittedAt; CreateSubmissionInfoBuilder() { CreateSubmissionInfo._initializeBuilder(this); } CreateSubmissionInfoBuilder get _$this { - if (_$v != null) { - _submissionType = _$v.submissionType; - _body = _$v.body; - _url = _$v.url; - _userId = _$v.userId; - _submittedAt = _$v.submittedAt; + final $v = _$v; + if ($v != null) { + _submissionType = $v.submissionType; + _body = $v.body; + _url = $v.url; + _userId = $v.userId; + _submittedAt = $v.submittedAt; _$v = null; } return this; @@ -207,19 +205,19 @@ class CreateSubmissionInfoBuilder @override void replace(CreateSubmissionInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateSubmissionInfo; } @override - void update(void Function(CreateSubmissionInfoBuilder) updates) { + void update(void Function(CreateSubmissionInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateSubmissionInfo build() { + CreateSubmissionInfo build() => _build(); + + _$CreateSubmissionInfo _build() { final _$result = _$v ?? new _$CreateSubmissionInfo._( submissionType: submissionType, @@ -232,4 +230,4 @@ class CreateSubmissionInfoBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_submission_wrapper.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_submission_wrapper.g.dart index 458c20706e..5735ee2af6 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_submission_wrapper.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_submission_wrapper.g.dart @@ -20,10 +20,10 @@ class _$CreateSubmissionWrapperSerializer final String wireName = 'CreateSubmissionWrapper'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, CreateSubmissionWrapper object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'submission', serializers.serialize(object.submission, specifiedType: const FullType(CreateSubmissionInfo)), @@ -34,20 +34,19 @@ class _$CreateSubmissionWrapperSerializer @override CreateSubmissionWrapper deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateSubmissionWrapperBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'submission': result.submission.replace(serializers.deserialize(value, - specifiedType: const FullType(CreateSubmissionInfo)) + specifiedType: const FullType(CreateSubmissionInfo))! as CreateSubmissionInfo); break; } @@ -62,14 +61,12 @@ class _$CreateSubmissionWrapper extends CreateSubmissionWrapper { final CreateSubmissionInfo submission; factory _$CreateSubmissionWrapper( - [void Function(CreateSubmissionWrapperBuilder) updates]) => - (new CreateSubmissionWrapperBuilder()..update(updates)).build(); + [void Function(CreateSubmissionWrapperBuilder)? updates]) => + (new CreateSubmissionWrapperBuilder()..update(updates))._build(); - _$CreateSubmissionWrapper._({this.submission}) : super._() { - if (submission == null) { - throw new BuiltValueNullFieldError( - 'CreateSubmissionWrapper', 'submission'); - } + _$CreateSubmissionWrapper._({required this.submission}) : super._() { + BuiltValueNullFieldError.checkNotNull( + submission, r'CreateSubmissionWrapper', 'submission'); } @override @@ -89,12 +86,15 @@ class _$CreateSubmissionWrapper extends CreateSubmissionWrapper { @override int get hashCode { - return $jf($jc(0, submission.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, submission.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateSubmissionWrapper') + return (newBuiltValueToStringHelper(r'CreateSubmissionWrapper') ..add('submission', submission)) .toString(); } @@ -103,12 +103,12 @@ class _$CreateSubmissionWrapper extends CreateSubmissionWrapper { class CreateSubmissionWrapperBuilder implements Builder { - _$CreateSubmissionWrapper _$v; + _$CreateSubmissionWrapper? _$v; - CreateSubmissionInfoBuilder _submission; + CreateSubmissionInfoBuilder? _submission; CreateSubmissionInfoBuilder get submission => _$this._submission ??= new CreateSubmissionInfoBuilder(); - set submission(CreateSubmissionInfoBuilder submission) => + set submission(CreateSubmissionInfoBuilder? submission) => _$this._submission = submission; CreateSubmissionWrapperBuilder() { @@ -116,8 +116,9 @@ class CreateSubmissionWrapperBuilder } CreateSubmissionWrapperBuilder get _$this { - if (_$v != null) { - _submission = _$v.submission?.toBuilder(); + final $v = _$v; + if ($v != null) { + _submission = $v.submission.toBuilder(); _$v = null; } return this; @@ -125,31 +126,31 @@ class CreateSubmissionWrapperBuilder @override void replace(CreateSubmissionWrapper other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateSubmissionWrapper; } @override - void update(void Function(CreateSubmissionWrapperBuilder) updates) { + void update(void Function(CreateSubmissionWrapperBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateSubmissionWrapper build() { + CreateSubmissionWrapper build() => _build(); + + _$CreateSubmissionWrapper _build() { _$CreateSubmissionWrapper _$result; try { _$result = _$v ?? new _$CreateSubmissionWrapper._(submission: submission.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'submission'; submission.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateSubmissionWrapper', _$failedField, e.toString()); + r'CreateSubmissionWrapper', _$failedField, e.toString()); } rethrow; } @@ -158,4 +159,4 @@ class CreateSubmissionWrapperBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/create_user_info.g.dart b/apps/flutter_parent/lib/models/dataseeding/create_user_info.g.dart index 385d23443a..758a8fed59 100644 --- a/apps/flutter_parent/lib/models/dataseeding/create_user_info.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/create_user_info.g.dart @@ -17,9 +17,9 @@ class _$CreateUserInfoSerializer final String wireName = 'CreateUserInfo'; @override - Iterable serialize(Serializers serializers, CreateUserInfo object, + Iterable serialize(Serializers serializers, CreateUserInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'user', serializers.serialize(object.user, specifiedType: const FullType(UserNameData)), @@ -36,28 +36,27 @@ class _$CreateUserInfoSerializer @override CreateUserInfo deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new CreateUserInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'user': result.user.replace(serializers.deserialize(value, - specifiedType: const FullType(UserNameData)) as UserNameData); + specifiedType: const FullType(UserNameData))! as UserNameData); break; case 'pseudonym': result.pseudonym.replace(serializers.deserialize(value, - specifiedType: const FullType(Pseudonym)) as Pseudonym); + specifiedType: const FullType(Pseudonym))! as Pseudonym); break; case 'communication_channel': result.communicationChannel.replace(serializers.deserialize(value, - specifiedType: const FullType(CommunicationChannel)) + specifiedType: const FullType(CommunicationChannel))! as CommunicationChannel); break; } @@ -75,21 +74,19 @@ class _$CreateUserInfo extends CreateUserInfo { @override final CommunicationChannel communicationChannel; - factory _$CreateUserInfo([void Function(CreateUserInfoBuilder) updates]) => - (new CreateUserInfoBuilder()..update(updates)).build(); + factory _$CreateUserInfo([void Function(CreateUserInfoBuilder)? updates]) => + (new CreateUserInfoBuilder()..update(updates))._build(); - _$CreateUserInfo._({this.user, this.pseudonym, this.communicationChannel}) + _$CreateUserInfo._( + {required this.user, + required this.pseudonym, + required this.communicationChannel}) : super._() { - if (user == null) { - throw new BuiltValueNullFieldError('CreateUserInfo', 'user'); - } - if (pseudonym == null) { - throw new BuiltValueNullFieldError('CreateUserInfo', 'pseudonym'); - } - if (communicationChannel == null) { - throw new BuiltValueNullFieldError( - 'CreateUserInfo', 'communicationChannel'); - } + BuiltValueNullFieldError.checkNotNull(user, r'CreateUserInfo', 'user'); + BuiltValueNullFieldError.checkNotNull( + pseudonym, r'CreateUserInfo', 'pseudonym'); + BuiltValueNullFieldError.checkNotNull( + communicationChannel, r'CreateUserInfo', 'communicationChannel'); } @override @@ -111,13 +108,17 @@ class _$CreateUserInfo extends CreateUserInfo { @override int get hashCode { - return $jf($jc($jc($jc(0, user.hashCode), pseudonym.hashCode), - communicationChannel.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, user.hashCode); + _$hash = $jc(_$hash, pseudonym.hashCode); + _$hash = $jc(_$hash, communicationChannel.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('CreateUserInfo') + return (newBuiltValueToStringHelper(r'CreateUserInfo') ..add('user', user) ..add('pseudonym', pseudonym) ..add('communicationChannel', communicationChannel)) @@ -127,21 +128,21 @@ class _$CreateUserInfo extends CreateUserInfo { class CreateUserInfoBuilder implements Builder { - _$CreateUserInfo _$v; + _$CreateUserInfo? _$v; - UserNameDataBuilder _user; + UserNameDataBuilder? _user; UserNameDataBuilder get user => _$this._user ??= new UserNameDataBuilder(); - set user(UserNameDataBuilder user) => _$this._user = user; + set user(UserNameDataBuilder? user) => _$this._user = user; - PseudonymBuilder _pseudonym; + PseudonymBuilder? _pseudonym; PseudonymBuilder get pseudonym => _$this._pseudonym ??= new PseudonymBuilder(); - set pseudonym(PseudonymBuilder pseudonym) => _$this._pseudonym = pseudonym; + set pseudonym(PseudonymBuilder? pseudonym) => _$this._pseudonym = pseudonym; - CommunicationChannelBuilder _communicationChannel; + CommunicationChannelBuilder? _communicationChannel; CommunicationChannelBuilder get communicationChannel => _$this._communicationChannel ??= new CommunicationChannelBuilder(); - set communicationChannel(CommunicationChannelBuilder communicationChannel) => + set communicationChannel(CommunicationChannelBuilder? communicationChannel) => _$this._communicationChannel = communicationChannel; CreateUserInfoBuilder() { @@ -149,10 +150,11 @@ class CreateUserInfoBuilder } CreateUserInfoBuilder get _$this { - if (_$v != null) { - _user = _$v.user?.toBuilder(); - _pseudonym = _$v.pseudonym?.toBuilder(); - _communicationChannel = _$v.communicationChannel?.toBuilder(); + final $v = _$v; + if ($v != null) { + _user = $v.user.toBuilder(); + _pseudonym = $v.pseudonym.toBuilder(); + _communicationChannel = $v.communicationChannel.toBuilder(); _$v = null; } return this; @@ -160,19 +162,19 @@ class CreateUserInfoBuilder @override void replace(CreateUserInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$CreateUserInfo; } @override - void update(void Function(CreateUserInfoBuilder) updates) { + void update(void Function(CreateUserInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$CreateUserInfo build() { + CreateUserInfo build() => _build(); + + _$CreateUserInfo _build() { _$CreateUserInfo _$result; try { _$result = _$v ?? @@ -181,7 +183,7 @@ class CreateUserInfoBuilder pseudonym: pseudonym.build(), communicationChannel: communicationChannel.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'user'; user.build(); @@ -191,7 +193,7 @@ class CreateUserInfoBuilder communicationChannel.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'CreateUserInfo', _$failedField, e.toString()); + r'CreateUserInfo', _$failedField, e.toString()); } rethrow; } @@ -200,4 +202,4 @@ class CreateUserInfoBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.dart b/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.dart index fc7b4727b5..4c4cba2965 100644 --- a/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.dart +++ b/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.dart @@ -27,8 +27,7 @@ abstract class GradeSubmissionInfo implements Built b; } diff --git a/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.g.dart b/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.g.dart index 3dc77e0bf2..0fd309db3f 100644 --- a/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/grade_submission_info.g.dart @@ -20,44 +20,43 @@ class _$GradeSubmissionInfoSerializer final String wireName = 'GradeSubmissionInfo'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, GradeSubmissionInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'posted_grade', serializers.serialize(object.postedGrade, specifiedType: const FullType(String)), ]; - result.add('excuse'); - if (object.excuse == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.excuse, - specifiedType: const FullType(bool))); - } + Object? value; + value = object.excuse; + + result + ..add('excuse') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + return result; } @override GradeSubmissionInfo deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new GradeSubmissionInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'posted_grade': result.postedGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'excuse': result.excuse = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; } } @@ -70,16 +69,16 @@ class _$GradeSubmissionInfo extends GradeSubmissionInfo { @override final String postedGrade; @override - final bool excuse; + final bool? excuse; factory _$GradeSubmissionInfo( - [void Function(GradeSubmissionInfoBuilder) updates]) => - (new GradeSubmissionInfoBuilder()..update(updates)).build(); + [void Function(GradeSubmissionInfoBuilder)? updates]) => + (new GradeSubmissionInfoBuilder()..update(updates))._build(); - _$GradeSubmissionInfo._({this.postedGrade, this.excuse}) : super._() { - if (postedGrade == null) { - throw new BuiltValueNullFieldError('GradeSubmissionInfo', 'postedGrade'); - } + _$GradeSubmissionInfo._({required this.postedGrade, this.excuse}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + postedGrade, r'GradeSubmissionInfo', 'postedGrade'); } @override @@ -101,12 +100,16 @@ class _$GradeSubmissionInfo extends GradeSubmissionInfo { @override int get hashCode { - return $jf($jc($jc(0, postedGrade.hashCode), excuse.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, postedGrade.hashCode); + _$hash = $jc(_$hash, excuse.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('GradeSubmissionInfo') + return (newBuiltValueToStringHelper(r'GradeSubmissionInfo') ..add('postedGrade', postedGrade) ..add('excuse', excuse)) .toString(); @@ -115,24 +118,25 @@ class _$GradeSubmissionInfo extends GradeSubmissionInfo { class GradeSubmissionInfoBuilder implements Builder { - _$GradeSubmissionInfo _$v; + _$GradeSubmissionInfo? _$v; - String _postedGrade; - String get postedGrade => _$this._postedGrade; - set postedGrade(String postedGrade) => _$this._postedGrade = postedGrade; + String? _postedGrade; + String? get postedGrade => _$this._postedGrade; + set postedGrade(String? postedGrade) => _$this._postedGrade = postedGrade; - bool _excuse; - bool get excuse => _$this._excuse; - set excuse(bool excuse) => _$this._excuse = excuse; + bool? _excuse; + bool? get excuse => _$this._excuse; + set excuse(bool? excuse) => _$this._excuse = excuse; GradeSubmissionInfoBuilder() { GradeSubmissionInfo._initializeBuilder(this); } GradeSubmissionInfoBuilder get _$this { - if (_$v != null) { - _postedGrade = _$v.postedGrade; - _excuse = _$v.excuse; + final $v = _$v; + if ($v != null) { + _postedGrade = $v.postedGrade; + _excuse = $v.excuse; _$v = null; } return this; @@ -140,24 +144,27 @@ class GradeSubmissionInfoBuilder @override void replace(GradeSubmissionInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$GradeSubmissionInfo; } @override - void update(void Function(GradeSubmissionInfoBuilder) updates) { + void update(void Function(GradeSubmissionInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$GradeSubmissionInfo build() { + GradeSubmissionInfo build() => _build(); + + _$GradeSubmissionInfo _build() { final _$result = _$v ?? - new _$GradeSubmissionInfo._(postedGrade: postedGrade, excuse: excuse); + new _$GradeSubmissionInfo._( + postedGrade: BuiltValueNullFieldError.checkNotNull( + postedGrade, r'GradeSubmissionInfo', 'postedGrade'), + excuse: excuse); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/grade_submission_wrapper.g.dart b/apps/flutter_parent/lib/models/dataseeding/grade_submission_wrapper.g.dart index ebab7dafb3..00093e37de 100644 --- a/apps/flutter_parent/lib/models/dataseeding/grade_submission_wrapper.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/grade_submission_wrapper.g.dart @@ -20,10 +20,10 @@ class _$GradeSubmissionWrapperSerializer final String wireName = 'GradeSubmissionWrapper'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, GradeSubmissionWrapper object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'submission', serializers.serialize(object.submission, specifiedType: const FullType(GradeSubmissionInfo)), @@ -34,20 +34,19 @@ class _$GradeSubmissionWrapperSerializer @override GradeSubmissionWrapper deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new GradeSubmissionWrapperBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'submission': result.submission.replace(serializers.deserialize(value, - specifiedType: const FullType(GradeSubmissionInfo)) + specifiedType: const FullType(GradeSubmissionInfo))! as GradeSubmissionInfo); break; } @@ -62,14 +61,12 @@ class _$GradeSubmissionWrapper extends GradeSubmissionWrapper { final GradeSubmissionInfo submission; factory _$GradeSubmissionWrapper( - [void Function(GradeSubmissionWrapperBuilder) updates]) => - (new GradeSubmissionWrapperBuilder()..update(updates)).build(); + [void Function(GradeSubmissionWrapperBuilder)? updates]) => + (new GradeSubmissionWrapperBuilder()..update(updates))._build(); - _$GradeSubmissionWrapper._({this.submission}) : super._() { - if (submission == null) { - throw new BuiltValueNullFieldError( - 'GradeSubmissionWrapper', 'submission'); - } + _$GradeSubmissionWrapper._({required this.submission}) : super._() { + BuiltValueNullFieldError.checkNotNull( + submission, r'GradeSubmissionWrapper', 'submission'); } @override @@ -89,12 +86,15 @@ class _$GradeSubmissionWrapper extends GradeSubmissionWrapper { @override int get hashCode { - return $jf($jc(0, submission.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, submission.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('GradeSubmissionWrapper') + return (newBuiltValueToStringHelper(r'GradeSubmissionWrapper') ..add('submission', submission)) .toString(); } @@ -102,12 +102,12 @@ class _$GradeSubmissionWrapper extends GradeSubmissionWrapper { class GradeSubmissionWrapperBuilder implements Builder { - _$GradeSubmissionWrapper _$v; + _$GradeSubmissionWrapper? _$v; - GradeSubmissionInfoBuilder _submission; + GradeSubmissionInfoBuilder? _submission; GradeSubmissionInfoBuilder get submission => _$this._submission ??= new GradeSubmissionInfoBuilder(); - set submission(GradeSubmissionInfoBuilder submission) => + set submission(GradeSubmissionInfoBuilder? submission) => _$this._submission = submission; GradeSubmissionWrapperBuilder() { @@ -115,8 +115,9 @@ class GradeSubmissionWrapperBuilder } GradeSubmissionWrapperBuilder get _$this { - if (_$v != null) { - _submission = _$v.submission?.toBuilder(); + final $v = _$v; + if ($v != null) { + _submission = $v.submission.toBuilder(); _$v = null; } return this; @@ -124,31 +125,31 @@ class GradeSubmissionWrapperBuilder @override void replace(GradeSubmissionWrapper other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$GradeSubmissionWrapper; } @override - void update(void Function(GradeSubmissionWrapperBuilder) updates) { + void update(void Function(GradeSubmissionWrapperBuilder)? updates) { if (updates != null) updates(this); } @override - _$GradeSubmissionWrapper build() { + GradeSubmissionWrapper build() => _build(); + + _$GradeSubmissionWrapper _build() { _$GradeSubmissionWrapper _$result; try { _$result = _$v ?? new _$GradeSubmissionWrapper._(submission: submission.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'submission'; submission.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'GradeSubmissionWrapper', _$failedField, e.toString()); + r'GradeSubmissionWrapper', _$failedField, e.toString()); } rethrow; } @@ -157,4 +158,4 @@ class GradeSubmissionWrapperBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/oauth_token.g.dart b/apps/flutter_parent/lib/models/dataseeding/oauth_token.g.dart index f35d7a61bd..b72dd51fd4 100644 --- a/apps/flutter_parent/lib/models/dataseeding/oauth_token.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/oauth_token.g.dart @@ -15,9 +15,9 @@ class _$OAuthTokenSerializer implements StructuredSerializer { final String wireName = 'OAuthToken'; @override - Iterable serialize(Serializers serializers, OAuthToken object, + Iterable serialize(Serializers serializers, OAuthToken object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'access_token', serializers.serialize(object.accessToken, specifiedType: const FullType(String)), @@ -27,20 +27,19 @@ class _$OAuthTokenSerializer implements StructuredSerializer { } @override - OAuthToken deserialize(Serializers serializers, Iterable serialized, + OAuthToken deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new OAuthTokenBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'access_token': result.accessToken = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -53,13 +52,12 @@ class _$OAuthToken extends OAuthToken { @override final String accessToken; - factory _$OAuthToken([void Function(OAuthTokenBuilder) updates]) => - (new OAuthTokenBuilder()..update(updates)).build(); + factory _$OAuthToken([void Function(OAuthTokenBuilder)? updates]) => + (new OAuthTokenBuilder()..update(updates))._build(); - _$OAuthToken._({this.accessToken}) : super._() { - if (accessToken == null) { - throw new BuiltValueNullFieldError('OAuthToken', 'accessToken'); - } + _$OAuthToken._({required this.accessToken}) : super._() { + BuiltValueNullFieldError.checkNotNull( + accessToken, r'OAuthToken', 'accessToken'); } @override @@ -77,31 +75,35 @@ class _$OAuthToken extends OAuthToken { @override int get hashCode { - return $jf($jc(0, accessToken.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, accessToken.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('OAuthToken') + return (newBuiltValueToStringHelper(r'OAuthToken') ..add('accessToken', accessToken)) .toString(); } } class OAuthTokenBuilder implements Builder { - _$OAuthToken _$v; + _$OAuthToken? _$v; - String _accessToken; - String get accessToken => _$this._accessToken; - set accessToken(String accessToken) => _$this._accessToken = accessToken; + String? _accessToken; + String? get accessToken => _$this._accessToken; + set accessToken(String? accessToken) => _$this._accessToken = accessToken; OAuthTokenBuilder() { OAuthToken._initializeBuilder(this); } OAuthTokenBuilder get _$this { - if (_$v != null) { - _accessToken = _$v.accessToken; + final $v = _$v; + if ($v != null) { + _accessToken = $v.accessToken; _$v = null; } return this; @@ -109,23 +111,26 @@ class OAuthTokenBuilder implements Builder { @override void replace(OAuthToken other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$OAuthToken; } @override - void update(void Function(OAuthTokenBuilder) updates) { + void update(void Function(OAuthTokenBuilder)? updates) { if (updates != null) updates(this); } @override - _$OAuthToken build() { - final _$result = _$v ?? new _$OAuthToken._(accessToken: accessToken); + OAuthToken build() => _build(); + + _$OAuthToken _build() { + final _$result = _$v ?? + new _$OAuthToken._( + accessToken: BuiltValueNullFieldError.checkNotNull( + accessToken, r'OAuthToken', 'accessToken')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/pairing_code.dart b/apps/flutter_parent/lib/models/dataseeding/pairing_code.dart index 16a7ea9642..38bd955245 100644 --- a/apps/flutter_parent/lib/models/dataseeding/pairing_code.dart +++ b/apps/flutter_parent/lib/models/dataseeding/pairing_code.dart @@ -26,9 +26,12 @@ abstract class PairingCode implements Built { @BuiltValueField(wireName: "user_id") String get userId; + String get code; + @BuiltValueField(wireName: "expires_at") String get expiresAt; + @BuiltValueField(wireName: "workflow_state") String get workflowState; diff --git a/apps/flutter_parent/lib/models/dataseeding/pairing_code.g.dart b/apps/flutter_parent/lib/models/dataseeding/pairing_code.g.dart index 7d92373ccf..fa881716d6 100644 --- a/apps/flutter_parent/lib/models/dataseeding/pairing_code.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/pairing_code.g.dart @@ -15,9 +15,9 @@ class _$PairingCodeSerializer implements StructuredSerializer { final String wireName = 'PairingCode'; @override - Iterable serialize(Serializers serializers, PairingCode object, + Iterable serialize(Serializers serializers, PairingCode object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'user_id', serializers.serialize(object.userId, specifiedType: const FullType(String)), @@ -35,32 +35,31 @@ class _$PairingCodeSerializer implements StructuredSerializer { } @override - PairingCode deserialize(Serializers serializers, Iterable serialized, + PairingCode deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PairingCodeBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'code': result.code = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'expires_at': result.expiresAt = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'workflow_state': result.workflowState = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -79,23 +78,21 @@ class _$PairingCode extends PairingCode { @override final String workflowState; - factory _$PairingCode([void Function(PairingCodeBuilder) updates]) => - (new PairingCodeBuilder()..update(updates)).build(); + factory _$PairingCode([void Function(PairingCodeBuilder)? updates]) => + (new PairingCodeBuilder()..update(updates))._build(); - _$PairingCode._({this.userId, this.code, this.expiresAt, this.workflowState}) + _$PairingCode._( + {required this.userId, + required this.code, + required this.expiresAt, + required this.workflowState}) : super._() { - if (userId == null) { - throw new BuiltValueNullFieldError('PairingCode', 'userId'); - } - if (code == null) { - throw new BuiltValueNullFieldError('PairingCode', 'code'); - } - if (expiresAt == null) { - throw new BuiltValueNullFieldError('PairingCode', 'expiresAt'); - } - if (workflowState == null) { - throw new BuiltValueNullFieldError('PairingCode', 'workflowState'); - } + BuiltValueNullFieldError.checkNotNull(userId, r'PairingCode', 'userId'); + BuiltValueNullFieldError.checkNotNull(code, r'PairingCode', 'code'); + BuiltValueNullFieldError.checkNotNull( + expiresAt, r'PairingCode', 'expiresAt'); + BuiltValueNullFieldError.checkNotNull( + workflowState, r'PairingCode', 'workflowState'); } @override @@ -117,14 +114,18 @@ class _$PairingCode extends PairingCode { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, userId.hashCode), code.hashCode), expiresAt.hashCode), - workflowState.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, code.hashCode); + _$hash = $jc(_$hash, expiresAt.hashCode); + _$hash = $jc(_$hash, workflowState.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('PairingCode') + return (newBuiltValueToStringHelper(r'PairingCode') ..add('userId', userId) ..add('code', code) ..add('expiresAt', expiresAt) @@ -134,23 +135,23 @@ class _$PairingCode extends PairingCode { } class PairingCodeBuilder implements Builder { - _$PairingCode _$v; + _$PairingCode? _$v; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _code; - String get code => _$this._code; - set code(String code) => _$this._code = code; + String? _code; + String? get code => _$this._code; + set code(String? code) => _$this._code = code; - String _expiresAt; - String get expiresAt => _$this._expiresAt; - set expiresAt(String expiresAt) => _$this._expiresAt = expiresAt; + String? _expiresAt; + String? get expiresAt => _$this._expiresAt; + set expiresAt(String? expiresAt) => _$this._expiresAt = expiresAt; - String _workflowState; - String get workflowState => _$this._workflowState; - set workflowState(String workflowState) => + String? _workflowState; + String? get workflowState => _$this._workflowState; + set workflowState(String? workflowState) => _$this._workflowState = workflowState; PairingCodeBuilder() { @@ -158,11 +159,12 @@ class PairingCodeBuilder implements Builder { } PairingCodeBuilder get _$this { - if (_$v != null) { - _userId = _$v.userId; - _code = _$v.code; - _expiresAt = _$v.expiresAt; - _workflowState = _$v.workflowState; + final $v = _$v; + if ($v != null) { + _userId = $v.userId; + _code = $v.code; + _expiresAt = $v.expiresAt; + _workflowState = $v.workflowState; _$v = null; } return this; @@ -170,28 +172,32 @@ class PairingCodeBuilder implements Builder { @override void replace(PairingCode other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PairingCode; } @override - void update(void Function(PairingCodeBuilder) updates) { + void update(void Function(PairingCodeBuilder)? updates) { if (updates != null) updates(this); } @override - _$PairingCode build() { + PairingCode build() => _build(); + + _$PairingCode _build() { final _$result = _$v ?? new _$PairingCode._( - userId: userId, - code: code, - expiresAt: expiresAt, - workflowState: workflowState); + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'PairingCode', 'userId'), + code: BuiltValueNullFieldError.checkNotNull( + code, r'PairingCode', 'code'), + expiresAt: BuiltValueNullFieldError.checkNotNull( + expiresAt, r'PairingCode', 'expiresAt'), + workflowState: BuiltValueNullFieldError.checkNotNull( + workflowState, r'PairingCode', 'workflowState')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/pseudonym.g.dart b/apps/flutter_parent/lib/models/dataseeding/pseudonym.g.dart index e1f09db85e..91af4243dd 100644 --- a/apps/flutter_parent/lib/models/dataseeding/pseudonym.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/pseudonym.g.dart @@ -15,9 +15,9 @@ class _$PseudonymSerializer implements StructuredSerializer { final String wireName = 'Pseudonym'; @override - Iterable serialize(Serializers serializers, Pseudonym object, + Iterable serialize(Serializers serializers, Pseudonym object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'unique_id', serializers.serialize(object.uniqueId, specifiedType: const FullType(String)), @@ -30,24 +30,23 @@ class _$PseudonymSerializer implements StructuredSerializer { } @override - Pseudonym deserialize(Serializers serializers, Iterable serialized, + Pseudonym deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PseudonymBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'unique_id': result.uniqueId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'password': result.password = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -62,16 +61,12 @@ class _$Pseudonym extends Pseudonym { @override final String password; - factory _$Pseudonym([void Function(PseudonymBuilder) updates]) => - (new PseudonymBuilder()..update(updates)).build(); + factory _$Pseudonym([void Function(PseudonymBuilder)? updates]) => + (new PseudonymBuilder()..update(updates))._build(); - _$Pseudonym._({this.uniqueId, this.password}) : super._() { - if (uniqueId == null) { - throw new BuiltValueNullFieldError('Pseudonym', 'uniqueId'); - } - if (password == null) { - throw new BuiltValueNullFieldError('Pseudonym', 'password'); - } + _$Pseudonym._({required this.uniqueId, required this.password}) : super._() { + BuiltValueNullFieldError.checkNotNull(uniqueId, r'Pseudonym', 'uniqueId'); + BuiltValueNullFieldError.checkNotNull(password, r'Pseudonym', 'password'); } @override @@ -91,12 +86,16 @@ class _$Pseudonym extends Pseudonym { @override int get hashCode { - return $jf($jc($jc(0, uniqueId.hashCode), password.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, uniqueId.hashCode); + _$hash = $jc(_$hash, password.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Pseudonym') + return (newBuiltValueToStringHelper(r'Pseudonym') ..add('uniqueId', uniqueId) ..add('password', password)) .toString(); @@ -104,24 +103,25 @@ class _$Pseudonym extends Pseudonym { } class PseudonymBuilder implements Builder { - _$Pseudonym _$v; + _$Pseudonym? _$v; - String _uniqueId; - String get uniqueId => _$this._uniqueId; - set uniqueId(String uniqueId) => _$this._uniqueId = uniqueId; + String? _uniqueId; + String? get uniqueId => _$this._uniqueId; + set uniqueId(String? uniqueId) => _$this._uniqueId = uniqueId; - String _password; - String get password => _$this._password; - set password(String password) => _$this._password = password; + String? _password; + String? get password => _$this._password; + set password(String? password) => _$this._password = password; PseudonymBuilder() { Pseudonym._initializeBuilder(this); } PseudonymBuilder get _$this { - if (_$v != null) { - _uniqueId = _$v.uniqueId; - _password = _$v.password; + final $v = _$v; + if ($v != null) { + _uniqueId = $v.uniqueId; + _password = $v.password; _$v = null; } return this; @@ -129,24 +129,28 @@ class PseudonymBuilder implements Builder { @override void replace(Pseudonym other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Pseudonym; } @override - void update(void Function(PseudonymBuilder) updates) { + void update(void Function(PseudonymBuilder)? updates) { if (updates != null) updates(this); } @override - _$Pseudonym build() { - final _$result = - _$v ?? new _$Pseudonym._(uniqueId: uniqueId, password: password); + Pseudonym build() => _build(); + + _$Pseudonym _build() { + final _$result = _$v ?? + new _$Pseudonym._( + uniqueId: BuiltValueNullFieldError.checkNotNull( + uniqueId, r'Pseudonym', 'uniqueId'), + password: BuiltValueNullFieldError.checkNotNull( + password, r'Pseudonym', 'password')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/quiz.dart b/apps/flutter_parent/lib/models/dataseeding/quiz.dart index 344c1e39a7..ce7413f631 100644 --- a/apps/flutter_parent/lib/models/dataseeding/quiz.dart +++ b/apps/flutter_parent/lib/models/dataseeding/quiz.dart @@ -25,10 +25,14 @@ abstract class Quiz implements Built { factory Quiz([void Function(QuizBuilder) updates]) = _$Quiz; String get id; + String get title; + String get description; + @BuiltValueField(wireName: 'due_at') DateTime get dueAt; + @BuiltValueField(wireName: 'points_possible') double get pointsPossible; diff --git a/apps/flutter_parent/lib/models/dataseeding/quiz.g.dart b/apps/flutter_parent/lib/models/dataseeding/quiz.g.dart index 1eb2cc7b41..86df8d0be2 100644 --- a/apps/flutter_parent/lib/models/dataseeding/quiz.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/quiz.g.dart @@ -15,9 +15,9 @@ class _$QuizSerializer implements StructuredSerializer { final String wireName = 'Quiz'; @override - Iterable serialize(Serializers serializers, Quiz object, + Iterable serialize(Serializers serializers, Quiz object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'title', @@ -38,36 +38,35 @@ class _$QuizSerializer implements StructuredSerializer { } @override - Quiz deserialize(Serializers serializers, Iterable serialized, + Quiz deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new QuizBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'description': result.description = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'due_at': result.dueAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime))! as DateTime; break; case 'points_possible': result.pointsPossible = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double))! as double; break; } } @@ -88,27 +87,22 @@ class _$Quiz extends Quiz { @override final double pointsPossible; - factory _$Quiz([void Function(QuizBuilder) updates]) => - (new QuizBuilder()..update(updates)).build(); + factory _$Quiz([void Function(QuizBuilder)? updates]) => + (new QuizBuilder()..update(updates))._build(); _$Quiz._( - {this.id, this.title, this.description, this.dueAt, this.pointsPossible}) + {required this.id, + required this.title, + required this.description, + required this.dueAt, + required this.pointsPossible}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Quiz', 'id'); - } - if (title == null) { - throw new BuiltValueNullFieldError('Quiz', 'title'); - } - if (description == null) { - throw new BuiltValueNullFieldError('Quiz', 'description'); - } - if (dueAt == null) { - throw new BuiltValueNullFieldError('Quiz', 'dueAt'); - } - if (pointsPossible == null) { - throw new BuiltValueNullFieldError('Quiz', 'pointsPossible'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Quiz', 'id'); + BuiltValueNullFieldError.checkNotNull(title, r'Quiz', 'title'); + BuiltValueNullFieldError.checkNotNull(description, r'Quiz', 'description'); + BuiltValueNullFieldError.checkNotNull(dueAt, r'Quiz', 'dueAt'); + BuiltValueNullFieldError.checkNotNull( + pointsPossible, r'Quiz', 'pointsPossible'); } @override @@ -131,15 +125,19 @@ class _$Quiz extends Quiz { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), title.hashCode), description.hashCode), - dueAt.hashCode), - pointsPossible.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, dueAt.hashCode); + _$hash = $jc(_$hash, pointsPossible.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Quiz') + return (newBuiltValueToStringHelper(r'Quiz') ..add('id', id) ..add('title', title) ..add('description', description) @@ -150,27 +148,27 @@ class _$Quiz extends Quiz { } class QuizBuilder implements Builder { - _$Quiz _$v; + _$Quiz? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - String _description; - String get description => _$this._description; - set description(String description) => _$this._description = description; + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; - DateTime _dueAt; - DateTime get dueAt => _$this._dueAt; - set dueAt(DateTime dueAt) => _$this._dueAt = dueAt; + DateTime? _dueAt; + DateTime? get dueAt => _$this._dueAt; + set dueAt(DateTime? dueAt) => _$this._dueAt = dueAt; - double _pointsPossible; - double get pointsPossible => _$this._pointsPossible; - set pointsPossible(double pointsPossible) => + double? _pointsPossible; + double? get pointsPossible => _$this._pointsPossible; + set pointsPossible(double? pointsPossible) => _$this._pointsPossible = pointsPossible; QuizBuilder() { @@ -178,12 +176,13 @@ class QuizBuilder implements Builder { } QuizBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _title = _$v.title; - _description = _$v.description; - _dueAt = _$v.dueAt; - _pointsPossible = _$v.pointsPossible; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _title = $v.title; + _description = $v.description; + _dueAt = $v.dueAt; + _pointsPossible = $v.pointsPossible; _$v = null; } return this; @@ -191,29 +190,33 @@ class QuizBuilder implements Builder { @override void replace(Quiz other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Quiz; } @override - void update(void Function(QuizBuilder) updates) { + void update(void Function(QuizBuilder)? updates) { if (updates != null) updates(this); } @override - _$Quiz build() { + Quiz build() => _build(); + + _$Quiz _build() { final _$result = _$v ?? new _$Quiz._( - id: id, - title: title, - description: description, - dueAt: dueAt, - pointsPossible: pointsPossible); + id: BuiltValueNullFieldError.checkNotNull(id, r'Quiz', 'id'), + title: + BuiltValueNullFieldError.checkNotNull(title, r'Quiz', 'title'), + description: BuiltValueNullFieldError.checkNotNull( + description, r'Quiz', 'description'), + dueAt: + BuiltValueNullFieldError.checkNotNull(dueAt, r'Quiz', 'dueAt'), + pointsPossible: BuiltValueNullFieldError.checkNotNull( + pointsPossible, r'Quiz', 'pointsPossible')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/seed_context.dart b/apps/flutter_parent/lib/models/dataseeding/seed_context.dart index a75871c111..a3bc218154 100644 --- a/apps/flutter_parent/lib/models/dataseeding/seed_context.dart +++ b/apps/flutter_parent/lib/models/dataseeding/seed_context.dart @@ -35,7 +35,7 @@ abstract class SeedContext implements Built { static void _initializeBuilder(SeedContextBuilder b) => b..seedingComplete = false; // Convenience method for extracting seed objects - T getNamedObject(String objectName) { - return deserialize(json.decode(seedObjects[objectName])); + T? getNamedObject(String objectName) { + return deserialize(json.decode(seedObjects[objectName] ?? '')); } } diff --git a/apps/flutter_parent/lib/models/dataseeding/seed_context.g.dart b/apps/flutter_parent/lib/models/dataseeding/seed_context.g.dart index 8dfedf3356..7ceba76ca9 100644 --- a/apps/flutter_parent/lib/models/dataseeding/seed_context.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/seed_context.g.dart @@ -15,9 +15,9 @@ class _$SeedContextSerializer implements StructuredSerializer { final String wireName = 'SeedContext'; @override - Iterable serialize(Serializers serializers, SeedContext object, + Iterable serialize(Serializers serializers, SeedContext object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'seedingComplete', serializers.serialize(object.seedingComplete, specifiedType: const FullType(bool)), @@ -31,25 +31,24 @@ class _$SeedContextSerializer implements StructuredSerializer { } @override - SeedContext deserialize(Serializers serializers, Iterable serialized, + SeedContext deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new SeedContextBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'seedingComplete': result.seedingComplete = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'seedObjects': result.seedObjects.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltMap, - const [const FullType(String), const FullType(String)]))); + const [const FullType(String), const FullType(String)]))!); break; } } @@ -64,16 +63,15 @@ class _$SeedContext extends SeedContext { @override final BuiltMap seedObjects; - factory _$SeedContext([void Function(SeedContextBuilder) updates]) => - (new SeedContextBuilder()..update(updates)).build(); + factory _$SeedContext([void Function(SeedContextBuilder)? updates]) => + (new SeedContextBuilder()..update(updates))._build(); - _$SeedContext._({this.seedingComplete, this.seedObjects}) : super._() { - if (seedingComplete == null) { - throw new BuiltValueNullFieldError('SeedContext', 'seedingComplete'); - } - if (seedObjects == null) { - throw new BuiltValueNullFieldError('SeedContext', 'seedObjects'); - } + _$SeedContext._({required this.seedingComplete, required this.seedObjects}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + seedingComplete, r'SeedContext', 'seedingComplete'); + BuiltValueNullFieldError.checkNotNull( + seedObjects, r'SeedContext', 'seedObjects'); } @override @@ -93,12 +91,16 @@ class _$SeedContext extends SeedContext { @override int get hashCode { - return $jf($jc($jc(0, seedingComplete.hashCode), seedObjects.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, seedingComplete.hashCode); + _$hash = $jc(_$hash, seedObjects.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('SeedContext') + return (newBuiltValueToStringHelper(r'SeedContext') ..add('seedingComplete', seedingComplete) ..add('seedObjects', seedObjects)) .toString(); @@ -106,17 +108,17 @@ class _$SeedContext extends SeedContext { } class SeedContextBuilder implements Builder { - _$SeedContext _$v; + _$SeedContext? _$v; - bool _seedingComplete; - bool get seedingComplete => _$this._seedingComplete; - set seedingComplete(bool seedingComplete) => + bool? _seedingComplete; + bool? get seedingComplete => _$this._seedingComplete; + set seedingComplete(bool? seedingComplete) => _$this._seedingComplete = seedingComplete; - MapBuilder _seedObjects; + MapBuilder? _seedObjects; MapBuilder get seedObjects => _$this._seedObjects ??= new MapBuilder(); - set seedObjects(MapBuilder seedObjects) => + set seedObjects(MapBuilder? seedObjects) => _$this._seedObjects = seedObjects; SeedContextBuilder() { @@ -124,9 +126,10 @@ class SeedContextBuilder implements Builder { } SeedContextBuilder get _$this { - if (_$v != null) { - _seedingComplete = _$v.seedingComplete; - _seedObjects = _$v.seedObjects?.toBuilder(); + final $v = _$v; + if ($v != null) { + _seedingComplete = $v.seedingComplete; + _seedObjects = $v.seedObjects.toBuilder(); _$v = null; } return this; @@ -134,33 +137,34 @@ class SeedContextBuilder implements Builder { @override void replace(SeedContext other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$SeedContext; } @override - void update(void Function(SeedContextBuilder) updates) { + void update(void Function(SeedContextBuilder)? updates) { if (updates != null) updates(this); } @override - _$SeedContext build() { + SeedContext build() => _build(); + + _$SeedContext _build() { _$SeedContext _$result; try { _$result = _$v ?? new _$SeedContext._( - seedingComplete: seedingComplete, + seedingComplete: BuiltValueNullFieldError.checkNotNull( + seedingComplete, r'SeedContext', 'seedingComplete'), seedObjects: seedObjects.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'seedObjects'; seedObjects.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'SeedContext', _$failedField, e.toString()); + r'SeedContext', _$failedField, e.toString()); } rethrow; } @@ -169,4 +173,4 @@ class SeedContextBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/seeded_user.dart b/apps/flutter_parent/lib/models/dataseeding/seeded_user.dart index 8520bb40a1..21f57f4596 100644 --- a/apps/flutter_parent/lib/models/dataseeding/seeded_user.dart +++ b/apps/flutter_parent/lib/models/dataseeding/seeded_user.dart @@ -28,26 +28,29 @@ abstract class SeededUser implements Built { factory SeededUser([void Function(SeededUserBuilder) updates]) = _$SeededUser; String get id; + String get name; + @BuiltValueField(wireName: "short_name") String get shortName; + @BuiltValueField(wireName: "sortable_name") String get sortableName; - @nullable + @BuiltValueField(wireName: "terms_of_use") - bool get termsOfUse; - @nullable + bool? get termsOfUse; + @BuiltValueField(wireName: "login_id") - String get loginId; - @nullable - String get password; - @nullable + String? get loginId; + + String? get password; + @BuiltValueField(wireName: "avatar_url") - String get avatarUrl; - @nullable - String get token; - @nullable - String get domain; + String? get avatarUrl; + + String? get token; + + String? get domain; static void _initializeBuilder(SeededUserBuilder b) => b..name = ''; diff --git a/apps/flutter_parent/lib/models/dataseeding/seeded_user.g.dart b/apps/flutter_parent/lib/models/dataseeding/seeded_user.g.dart index 39e5e4a020..f49bcf7751 100644 --- a/apps/flutter_parent/lib/models/dataseeding/seeded_user.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/seeded_user.g.dart @@ -15,9 +15,9 @@ class _$SeededUserSerializer implements StructuredSerializer { final String wireName = 'SeededUser'; @override - Iterable serialize(Serializers serializers, SeededUser object, + Iterable serialize(Serializers serializers, SeededUser object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', @@ -29,102 +29,96 @@ class _$SeededUserSerializer implements StructuredSerializer { serializers.serialize(object.sortableName, specifiedType: const FullType(String)), ]; - result.add('terms_of_use'); - if (object.termsOfUse == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.termsOfUse, - specifiedType: const FullType(bool))); - } - result.add('login_id'); - if (object.loginId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.loginId, - specifiedType: const FullType(String))); - } - result.add('password'); - if (object.password == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.password, - specifiedType: const FullType(String))); - } - result.add('avatar_url'); - if (object.avatarUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.avatarUrl, - specifiedType: const FullType(String))); - } - result.add('token'); - if (object.token == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.token, - specifiedType: const FullType(String))); - } - result.add('domain'); - if (object.domain == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.domain, - specifiedType: const FullType(String))); - } + Object? value; + value = object.termsOfUse; + + result + ..add('terms_of_use') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.loginId; + + result + ..add('login_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.password; + + result + ..add('password') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.avatarUrl; + + result + ..add('avatar_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.token; + + result + ..add('token') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.domain; + + result + ..add('domain') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - SeededUser deserialize(Serializers serializers, Iterable serialized, + SeededUser deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new SeededUserBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'short_name': result.shortName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'sortable_name': result.sortableName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'terms_of_use': result.termsOfUse = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'login_id': result.loginId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'password': result.password = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'avatar_url': result.avatarUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'token': result.token = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'domain': result.domain = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -143,26 +137,26 @@ class _$SeededUser extends SeededUser { @override final String sortableName; @override - final bool termsOfUse; + final bool? termsOfUse; @override - final String loginId; + final String? loginId; @override - final String password; + final String? password; @override - final String avatarUrl; + final String? avatarUrl; @override - final String token; + final String? token; @override - final String domain; + final String? domain; - factory _$SeededUser([void Function(SeededUserBuilder) updates]) => - (new SeededUserBuilder()..update(updates)).build(); + factory _$SeededUser([void Function(SeededUserBuilder)? updates]) => + (new SeededUserBuilder()..update(updates))._build(); _$SeededUser._( - {this.id, - this.name, - this.shortName, - this.sortableName, + {required this.id, + required this.name, + required this.shortName, + required this.sortableName, this.termsOfUse, this.loginId, this.password, @@ -170,18 +164,12 @@ class _$SeededUser extends SeededUser { this.token, this.domain}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('SeededUser', 'id'); - } - if (name == null) { - throw new BuiltValueNullFieldError('SeededUser', 'name'); - } - if (shortName == null) { - throw new BuiltValueNullFieldError('SeededUser', 'shortName'); - } - if (sortableName == null) { - throw new BuiltValueNullFieldError('SeededUser', 'sortableName'); - } + BuiltValueNullFieldError.checkNotNull(id, r'SeededUser', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'SeededUser', 'name'); + BuiltValueNullFieldError.checkNotNull( + shortName, r'SeededUser', 'shortName'); + BuiltValueNullFieldError.checkNotNull( + sortableName, r'SeededUser', 'sortableName'); } @override @@ -209,27 +197,24 @@ class _$SeededUser extends SeededUser { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), name.hashCode), - shortName.hashCode), - sortableName.hashCode), - termsOfUse.hashCode), - loginId.hashCode), - password.hashCode), - avatarUrl.hashCode), - token.hashCode), - domain.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, shortName.hashCode); + _$hash = $jc(_$hash, sortableName.hashCode); + _$hash = $jc(_$hash, termsOfUse.hashCode); + _$hash = $jc(_$hash, loginId.hashCode); + _$hash = $jc(_$hash, password.hashCode); + _$hash = $jc(_$hash, avatarUrl.hashCode); + _$hash = $jc(_$hash, token.hashCode); + _$hash = $jc(_$hash, domain.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('SeededUser') + return (newBuiltValueToStringHelper(r'SeededUser') ..add('id', id) ..add('name', name) ..add('shortName', shortName) @@ -245,64 +230,65 @@ class _$SeededUser extends SeededUser { } class SeededUserBuilder implements Builder { - _$SeededUser _$v; + _$SeededUser? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _shortName; - String get shortName => _$this._shortName; - set shortName(String shortName) => _$this._shortName = shortName; + String? _shortName; + String? get shortName => _$this._shortName; + set shortName(String? shortName) => _$this._shortName = shortName; - String _sortableName; - String get sortableName => _$this._sortableName; - set sortableName(String sortableName) => _$this._sortableName = sortableName; + String? _sortableName; + String? get sortableName => _$this._sortableName; + set sortableName(String? sortableName) => _$this._sortableName = sortableName; - bool _termsOfUse; - bool get termsOfUse => _$this._termsOfUse; - set termsOfUse(bool termsOfUse) => _$this._termsOfUse = termsOfUse; + bool? _termsOfUse; + bool? get termsOfUse => _$this._termsOfUse; + set termsOfUse(bool? termsOfUse) => _$this._termsOfUse = termsOfUse; - String _loginId; - String get loginId => _$this._loginId; - set loginId(String loginId) => _$this._loginId = loginId; + String? _loginId; + String? get loginId => _$this._loginId; + set loginId(String? loginId) => _$this._loginId = loginId; - String _password; - String get password => _$this._password; - set password(String password) => _$this._password = password; + String? _password; + String? get password => _$this._password; + set password(String? password) => _$this._password = password; - String _avatarUrl; - String get avatarUrl => _$this._avatarUrl; - set avatarUrl(String avatarUrl) => _$this._avatarUrl = avatarUrl; + String? _avatarUrl; + String? get avatarUrl => _$this._avatarUrl; + set avatarUrl(String? avatarUrl) => _$this._avatarUrl = avatarUrl; - String _token; - String get token => _$this._token; - set token(String token) => _$this._token = token; + String? _token; + String? get token => _$this._token; + set token(String? token) => _$this._token = token; - String _domain; - String get domain => _$this._domain; - set domain(String domain) => _$this._domain = domain; + String? _domain; + String? get domain => _$this._domain; + set domain(String? domain) => _$this._domain = domain; SeededUserBuilder() { SeededUser._initializeBuilder(this); } SeededUserBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _shortName = _$v.shortName; - _sortableName = _$v.sortableName; - _termsOfUse = _$v.termsOfUse; - _loginId = _$v.loginId; - _password = _$v.password; - _avatarUrl = _$v.avatarUrl; - _token = _$v.token; - _domain = _$v.domain; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _shortName = $v.shortName; + _sortableName = $v.sortableName; + _termsOfUse = $v.termsOfUse; + _loginId = $v.loginId; + _password = $v.password; + _avatarUrl = $v.avatarUrl; + _token = $v.token; + _domain = $v.domain; _$v = null; } return this; @@ -310,25 +296,28 @@ class SeededUserBuilder implements Builder { @override void replace(SeededUser other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$SeededUser; } @override - void update(void Function(SeededUserBuilder) updates) { + void update(void Function(SeededUserBuilder)? updates) { if (updates != null) updates(this); } @override - _$SeededUser build() { + SeededUser build() => _build(); + + _$SeededUser _build() { final _$result = _$v ?? new _$SeededUser._( - id: id, - name: name, - shortName: shortName, - sortableName: sortableName, + id: BuiltValueNullFieldError.checkNotNull(id, r'SeededUser', 'id'), + name: BuiltValueNullFieldError.checkNotNull( + name, r'SeededUser', 'name'), + shortName: BuiltValueNullFieldError.checkNotNull( + shortName, r'SeededUser', 'shortName'), + sortableName: BuiltValueNullFieldError.checkNotNull( + sortableName, r'SeededUser', 'sortableName'), termsOfUse: termsOfUse, loginId: loginId, password: password, @@ -340,4 +329,4 @@ class SeededUserBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/dataseeding/user_name_data.dart b/apps/flutter_parent/lib/models/dataseeding/user_name_data.dart index 5debfde5cb..1da3226ce8 100644 --- a/apps/flutter_parent/lib/models/dataseeding/user_name_data.dart +++ b/apps/flutter_parent/lib/models/dataseeding/user_name_data.dart @@ -25,13 +25,15 @@ abstract class UserNameData implements Built factory UserNameData([void Function(UserNameDataBuilder) updates]) = _$UserNameData; String get name; + @BuiltValueField(wireName: "short_name") String get shortName; + @BuiltValueField(wireName: "sortable_name") String get sortableName; - @nullable + @BuiltValueField(wireName: "terms_of_use") - bool get termsOfUse; + bool? get termsOfUse; static void _initializeBuilder(UserNameDataBuilder b) => b..name = ''; } diff --git a/apps/flutter_parent/lib/models/dataseeding/user_name_data.g.dart b/apps/flutter_parent/lib/models/dataseeding/user_name_data.g.dart index b4359a3918..e75e6e7ecb 100644 --- a/apps/flutter_parent/lib/models/dataseeding/user_name_data.g.dart +++ b/apps/flutter_parent/lib/models/dataseeding/user_name_data.g.dart @@ -16,9 +16,9 @@ class _$UserNameDataSerializer implements StructuredSerializer { final String wireName = 'UserNameData'; @override - Iterable serialize(Serializers serializers, UserNameData object, + Iterable serialize(Serializers serializers, UserNameData object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), 'short_name', @@ -28,43 +28,43 @@ class _$UserNameDataSerializer implements StructuredSerializer { serializers.serialize(object.sortableName, specifiedType: const FullType(String)), ]; - result.add('terms_of_use'); - if (object.termsOfUse == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.termsOfUse, - specifiedType: const FullType(bool))); - } + Object? value; + value = object.termsOfUse; + + result + ..add('terms_of_use') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + return result; } @override - UserNameData deserialize(Serializers serializers, Iterable serialized, + UserNameData deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new UserNameDataBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'short_name': result.shortName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'sortable_name': result.sortableName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'terms_of_use': result.termsOfUse = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; } } @@ -81,23 +81,22 @@ class _$UserNameData extends UserNameData { @override final String sortableName; @override - final bool termsOfUse; + final bool? termsOfUse; - factory _$UserNameData([void Function(UserNameDataBuilder) updates]) => - (new UserNameDataBuilder()..update(updates)).build(); + factory _$UserNameData([void Function(UserNameDataBuilder)? updates]) => + (new UserNameDataBuilder()..update(updates))._build(); _$UserNameData._( - {this.name, this.shortName, this.sortableName, this.termsOfUse}) + {required this.name, + required this.shortName, + required this.sortableName, + this.termsOfUse}) : super._() { - if (name == null) { - throw new BuiltValueNullFieldError('UserNameData', 'name'); - } - if (shortName == null) { - throw new BuiltValueNullFieldError('UserNameData', 'shortName'); - } - if (sortableName == null) { - throw new BuiltValueNullFieldError('UserNameData', 'sortableName'); - } + BuiltValueNullFieldError.checkNotNull(name, r'UserNameData', 'name'); + BuiltValueNullFieldError.checkNotNull( + shortName, r'UserNameData', 'shortName'); + BuiltValueNullFieldError.checkNotNull( + sortableName, r'UserNameData', 'sortableName'); } @override @@ -119,15 +118,18 @@ class _$UserNameData extends UserNameData { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, name.hashCode), shortName.hashCode), - sortableName.hashCode), - termsOfUse.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, shortName.hashCode); + _$hash = $jc(_$hash, sortableName.hashCode); + _$hash = $jc(_$hash, termsOfUse.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('UserNameData') + return (newBuiltValueToStringHelper(r'UserNameData') ..add('name', name) ..add('shortName', shortName) ..add('sortableName', sortableName) @@ -138,34 +140,35 @@ class _$UserNameData extends UserNameData { class UserNameDataBuilder implements Builder { - _$UserNameData _$v; + _$UserNameData? _$v; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _shortName; - String get shortName => _$this._shortName; - set shortName(String shortName) => _$this._shortName = shortName; + String? _shortName; + String? get shortName => _$this._shortName; + set shortName(String? shortName) => _$this._shortName = shortName; - String _sortableName; - String get sortableName => _$this._sortableName; - set sortableName(String sortableName) => _$this._sortableName = sortableName; + String? _sortableName; + String? get sortableName => _$this._sortableName; + set sortableName(String? sortableName) => _$this._sortableName = sortableName; - bool _termsOfUse; - bool get termsOfUse => _$this._termsOfUse; - set termsOfUse(bool termsOfUse) => _$this._termsOfUse = termsOfUse; + bool? _termsOfUse; + bool? get termsOfUse => _$this._termsOfUse; + set termsOfUse(bool? termsOfUse) => _$this._termsOfUse = termsOfUse; UserNameDataBuilder() { UserNameData._initializeBuilder(this); } UserNameDataBuilder get _$this { - if (_$v != null) { - _name = _$v.name; - _shortName = _$v.shortName; - _sortableName = _$v.sortableName; - _termsOfUse = _$v.termsOfUse; + final $v = _$v; + if ($v != null) { + _name = $v.name; + _shortName = $v.shortName; + _sortableName = $v.sortableName; + _termsOfUse = $v.termsOfUse; _$v = null; } return this; @@ -173,28 +176,31 @@ class UserNameDataBuilder @override void replace(UserNameData other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$UserNameData; } @override - void update(void Function(UserNameDataBuilder) updates) { + void update(void Function(UserNameDataBuilder)? updates) { if (updates != null) updates(this); } @override - _$UserNameData build() { + UserNameData build() => _build(); + + _$UserNameData _build() { final _$result = _$v ?? new _$UserNameData._( - name: name, - shortName: shortName, - sortableName: sortableName, + name: BuiltValueNullFieldError.checkNotNull( + name, r'UserNameData', 'name'), + shortName: BuiltValueNullFieldError.checkNotNull( + shortName, r'UserNameData', 'shortName'), + sortableName: BuiltValueNullFieldError.checkNotNull( + sortableName, r'UserNameData', 'sortableName'), termsOfUse: termsOfUse); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/enrollment.dart b/apps/flutter_parent/lib/models/enrollment.dart index ed0cfc91b7..0a0c2d8afb 100644 --- a/apps/flutter_parent/lib/models/enrollment.dart +++ b/apps/flutter_parent/lib/models/enrollment.dart @@ -32,24 +32,18 @@ abstract class Enrollment implements Built { factory Enrollment([void Function(EnrollmentBuilder) updates]) = _$Enrollment; // The enrollment role, for course-level permissions - this field will match `type` if the enrollment role has not been customized - @nullable - String get role; + String? get role; - @nullable - String get type; + String? get type; String get id; // Only included when we get enrollments using the user's url: /users/self/enrollments - @nullable @BuiltValueField(wireName: 'course_id') - @nullable - String get courseId; + String? get courseId; - @nullable @BuiltValueField(wireName: 'course_section_id') - @nullable - String get courseSectionId; + String? get courseSectionId; @BuiltValueField(wireName: 'enrollment_state') String get enrollmentState; @@ -57,29 +51,23 @@ abstract class Enrollment implements Built { @BuiltValueField(wireName: 'user_id') String get userId; - @nullable - Grade get grades; + Grade? get grades; // Only included when we get the enrollment with a course object - @nullable @BuiltValueField(wireName: 'computed_current_score') - double get computedCurrentScore; + double? get computedCurrentScore; - @nullable @BuiltValueField(wireName: 'computed_final_score') - double get computedFinalScore; + double? get computedFinalScore; - @nullable @BuiltValueField(wireName: 'computed_current_grade') - String get computedCurrentGrade; + String? get computedCurrentGrade; - @nullable @BuiltValueField(wireName: 'computed_final_grade') - String get computedFinalGrade; + String? get computedFinalGrade; - @nullable @BuiltValueField(wireName: 'computed_current_letter_grade') - String get computedCurrentLetterGrade; + String? get computedCurrentLetterGrade; @BuiltValueField(wireName: 'multiple_grading_periods_enabled') bool get multipleGradingPeriodsEnabled; @@ -87,46 +75,37 @@ abstract class Enrollment implements Built { @BuiltValueField(wireName: 'totals_for_all_grading_periods_option') bool get totalsForAllGradingPeriodsOption; - @nullable @BuiltValueField(wireName: 'current_period_computed_current_score') - double get currentPeriodComputedCurrentScore; + double? get currentPeriodComputedCurrentScore; - @nullable @BuiltValueField(wireName: 'current_period_computed_final_score') - double get currentPeriodComputedFinalScore; + double? get currentPeriodComputedFinalScore; - @nullable @BuiltValueField(wireName: 'current_period_computed_current_grade') - String get currentPeriodComputedCurrentGrade; + String? get currentPeriodComputedCurrentGrade; - @nullable @BuiltValueField(wireName: 'current_period_computed_final_grade') - String get currentPeriodComputedFinalGrade; + String? get currentPeriodComputedFinalGrade; - @nullable @BuiltValueField(wireName: 'current_grading_period_id') - String get currentGradingPeriodId; + String? get currentGradingPeriodId; - @nullable @BuiltValueField(wireName: 'current_grading_period_title') - String get currentGradingPeriodTitle; + String? get currentGradingPeriodTitle; @BuiltValueField(wireName: 'associated_user_id') String get associatedUserId; // The unique id of the associated user. Will be null unless type is ObserverEnrollment. - @nullable @BuiltValueField(wireName: 'last_activity_at') - DateTime get lastActivityAt; + DateTime? get lastActivityAt; @BuiltValueField(wireName: 'limit_privileges_to_course_section') bool get limitPrivilegesToCourseSection; - @nullable @BuiltValueField(wireName: 'observed_user') - User get observedUser; + User? get observedUser; - @nullable - User get user; + User? get user; // Helper functions bool _matchesEnrollment(value) => value == type || value == role; @@ -144,7 +123,7 @@ abstract class Enrollment implements Built { bool hasActiveGradingPeriod() => multipleGradingPeriodsEnabled && currentGradingPeriodId != null && - currentGradingPeriodId.isNotEmpty && + currentGradingPeriodId?.isNotEmpty == true && currentGradingPeriodId != '0'; // NOTE: Looks like the API will never return multipleGradingPeriodsEnabled for observer enrollments, still checking just in case diff --git a/apps/flutter_parent/lib/models/enrollment.g.dart b/apps/flutter_parent/lib/models/enrollment.g.dart index 00202c3e20..313d4a7b83 100644 --- a/apps/flutter_parent/lib/models/enrollment.g.dart +++ b/apps/flutter_parent/lib/models/enrollment.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of enrollment; +part of 'enrollment.dart'; // ************************************************************************** // BuiltValueGenerator @@ -15,9 +15,9 @@ class _$EnrollmentSerializer implements StructuredSerializer { final String wireName = 'Enrollment'; @override - Iterable serialize(Serializers serializers, Enrollment object, + Iterable serialize(Serializers serializers, Enrollment object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'enrollment_state', @@ -39,7 +39,7 @@ class _$EnrollmentSerializer implements StructuredSerializer { serializers.serialize(object.limitPrivilegesToCourseSection, specifiedType: const FullType(bool)), ]; - Object value; + Object? value; value = object.role; result @@ -156,123 +156,123 @@ class _$EnrollmentSerializer implements StructuredSerializer { } @override - Enrollment deserialize(Serializers serializers, Iterable serialized, + Enrollment deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new EnrollmentBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'role': result.role = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'course_id': result.courseId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'course_section_id': result.courseSectionId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'enrollment_state': result.enrollmentState = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'grades': result.grades.replace(serializers.deserialize(value, - specifiedType: const FullType(Grade)) as Grade); + specifiedType: const FullType(Grade))! as Grade); break; case 'computed_current_score': result.computedCurrentScore = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'computed_final_score': result.computedFinalScore = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'computed_current_grade': result.computedCurrentGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'computed_final_grade': result.computedFinalGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'computed_current_letter_grade': result.computedCurrentLetterGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'multiple_grading_periods_enabled': result.multipleGradingPeriodsEnabled = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'totals_for_all_grading_periods_option': result.totalsForAllGradingPeriodsOption = serializers - .deserialize(value, specifiedType: const FullType(bool)) as bool; + .deserialize(value, specifiedType: const FullType(bool))! as bool; break; case 'current_period_computed_current_score': result.currentPeriodComputedCurrentScore = serializers.deserialize( value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'current_period_computed_final_score': result.currentPeriodComputedFinalScore = serializers.deserialize( value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'current_period_computed_current_grade': result.currentPeriodComputedCurrentGrade = serializers.deserialize( value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'current_period_computed_final_grade': result.currentPeriodComputedFinalGrade = serializers.deserialize( value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'current_grading_period_id': result.currentGradingPeriodId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'current_grading_period_title': result.currentGradingPeriodTitle = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'associated_user_id': result.associatedUserId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'last_activity_at': result.lastActivityAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'limit_privileges_to_course_section': result.limitPrivilegesToCourseSection = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'observed_user': result.observedUser.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; case 'user': result.user.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; } } @@ -283,101 +283,101 @@ class _$EnrollmentSerializer implements StructuredSerializer { class _$Enrollment extends Enrollment { @override - final String role; + final String? role; @override - final String type; + final String? type; @override final String id; @override - final String courseId; + final String? courseId; @override - final String courseSectionId; + final String? courseSectionId; @override final String enrollmentState; @override final String userId; @override - final Grade grades; + final Grade? grades; @override - final double computedCurrentScore; + final double? computedCurrentScore; @override - final double computedFinalScore; + final double? computedFinalScore; @override - final String computedCurrentGrade; + final String? computedCurrentGrade; @override - final String computedFinalGrade; + final String? computedFinalGrade; @override - final String computedCurrentLetterGrade; + final String? computedCurrentLetterGrade; @override final bool multipleGradingPeriodsEnabled; @override final bool totalsForAllGradingPeriodsOption; @override - final double currentPeriodComputedCurrentScore; + final double? currentPeriodComputedCurrentScore; @override - final double currentPeriodComputedFinalScore; + final double? currentPeriodComputedFinalScore; @override - final String currentPeriodComputedCurrentGrade; + final String? currentPeriodComputedCurrentGrade; @override - final String currentPeriodComputedFinalGrade; + final String? currentPeriodComputedFinalGrade; @override - final String currentGradingPeriodId; + final String? currentGradingPeriodId; @override - final String currentGradingPeriodTitle; + final String? currentGradingPeriodTitle; @override final String associatedUserId; @override - final DateTime lastActivityAt; + final DateTime? lastActivityAt; @override final bool limitPrivilegesToCourseSection; @override - final User observedUser; + final User? observedUser; @override - final User user; + final User? user; - factory _$Enrollment([void Function(EnrollmentBuilder) updates]) => - (new EnrollmentBuilder()..update(updates)).build(); + factory _$Enrollment([void Function(EnrollmentBuilder)? updates]) => + (new EnrollmentBuilder()..update(updates))._build(); _$Enrollment._( {this.role, this.type, - this.id, + required this.id, this.courseId, this.courseSectionId, - this.enrollmentState, - this.userId, + required this.enrollmentState, + required this.userId, this.grades, this.computedCurrentScore, this.computedFinalScore, this.computedCurrentGrade, this.computedFinalGrade, this.computedCurrentLetterGrade, - this.multipleGradingPeriodsEnabled, - this.totalsForAllGradingPeriodsOption, + required this.multipleGradingPeriodsEnabled, + required this.totalsForAllGradingPeriodsOption, this.currentPeriodComputedCurrentScore, this.currentPeriodComputedFinalScore, this.currentPeriodComputedCurrentGrade, this.currentPeriodComputedFinalGrade, this.currentGradingPeriodId, this.currentGradingPeriodTitle, - this.associatedUserId, + required this.associatedUserId, this.lastActivityAt, - this.limitPrivilegesToCourseSection, + required this.limitPrivilegesToCourseSection, this.observedUser, this.user}) : super._() { - BuiltValueNullFieldError.checkNotNull(id, 'Enrollment', 'id'); + BuiltValueNullFieldError.checkNotNull(id, r'Enrollment', 'id'); BuiltValueNullFieldError.checkNotNull( - enrollmentState, 'Enrollment', 'enrollmentState'); - BuiltValueNullFieldError.checkNotNull(userId, 'Enrollment', 'userId'); + enrollmentState, r'Enrollment', 'enrollmentState'); + BuiltValueNullFieldError.checkNotNull(userId, r'Enrollment', 'userId'); BuiltValueNullFieldError.checkNotNull(multipleGradingPeriodsEnabled, - 'Enrollment', 'multipleGradingPeriodsEnabled'); + r'Enrollment', 'multipleGradingPeriodsEnabled'); BuiltValueNullFieldError.checkNotNull(totalsForAllGradingPeriodsOption, - 'Enrollment', 'totalsForAllGradingPeriodsOption'); + r'Enrollment', 'totalsForAllGradingPeriodsOption'); BuiltValueNullFieldError.checkNotNull( - associatedUserId, 'Enrollment', 'associatedUserId'); + associatedUserId, r'Enrollment', 'associatedUserId'); BuiltValueNullFieldError.checkNotNull(limitPrivilegesToCourseSection, - 'Enrollment', 'limitPrivilegesToCourseSection'); + r'Enrollment', 'limitPrivilegesToCourseSection'); } @override @@ -427,49 +427,40 @@ class _$Enrollment extends Enrollment { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc($jc($jc($jc($jc($jc(0, role.hashCode), type.hashCode), id.hashCode), courseId.hashCode), courseSectionId.hashCode), enrollmentState.hashCode), userId.hashCode), - grades.hashCode), - computedCurrentScore.hashCode), - computedFinalScore.hashCode), - computedCurrentGrade.hashCode), - computedFinalGrade.hashCode), - computedCurrentLetterGrade.hashCode), - multipleGradingPeriodsEnabled.hashCode), - totalsForAllGradingPeriodsOption.hashCode), - currentPeriodComputedCurrentScore.hashCode), - currentPeriodComputedFinalScore.hashCode), - currentPeriodComputedCurrentGrade.hashCode), - currentPeriodComputedFinalGrade.hashCode), - currentGradingPeriodId.hashCode), - currentGradingPeriodTitle.hashCode), - associatedUserId.hashCode), - lastActivityAt.hashCode), - limitPrivilegesToCourseSection.hashCode), - observedUser.hashCode), - user.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, role.hashCode); + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, courseId.hashCode); + _$hash = $jc(_$hash, courseSectionId.hashCode); + _$hash = $jc(_$hash, enrollmentState.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, grades.hashCode); + _$hash = $jc(_$hash, computedCurrentScore.hashCode); + _$hash = $jc(_$hash, computedFinalScore.hashCode); + _$hash = $jc(_$hash, computedCurrentGrade.hashCode); + _$hash = $jc(_$hash, computedFinalGrade.hashCode); + _$hash = $jc(_$hash, computedCurrentLetterGrade.hashCode); + _$hash = $jc(_$hash, multipleGradingPeriodsEnabled.hashCode); + _$hash = $jc(_$hash, totalsForAllGradingPeriodsOption.hashCode); + _$hash = $jc(_$hash, currentPeriodComputedCurrentScore.hashCode); + _$hash = $jc(_$hash, currentPeriodComputedFinalScore.hashCode); + _$hash = $jc(_$hash, currentPeriodComputedCurrentGrade.hashCode); + _$hash = $jc(_$hash, currentPeriodComputedFinalGrade.hashCode); + _$hash = $jc(_$hash, currentGradingPeriodId.hashCode); + _$hash = $jc(_$hash, currentGradingPeriodTitle.hashCode); + _$hash = $jc(_$hash, associatedUserId.hashCode); + _$hash = $jc(_$hash, lastActivityAt.hashCode); + _$hash = $jc(_$hash, limitPrivilegesToCourseSection.hashCode); + _$hash = $jc(_$hash, observedUser.hashCode); + _$hash = $jc(_$hash, user.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Enrollment') + return (newBuiltValueToStringHelper(r'Enrollment') ..add('role', role) ..add('type', type) ..add('id', id) @@ -507,142 +498,145 @@ class _$Enrollment extends Enrollment { } class EnrollmentBuilder implements Builder { - _$Enrollment _$v; + _$Enrollment? _$v; - String _role; - String get role => _$this._role; - set role(String role) => _$this._role = role; + String? _role; + String? get role => _$this._role; + set role(String? role) => _$this._role = role; - String _type; - String get type => _$this._type; - set type(String type) => _$this._type = type; + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _courseId; - String get courseId => _$this._courseId; - set courseId(String courseId) => _$this._courseId = courseId; + String? _courseId; + String? get courseId => _$this._courseId; + set courseId(String? courseId) => _$this._courseId = courseId; - String _courseSectionId; - String get courseSectionId => _$this._courseSectionId; - set courseSectionId(String courseSectionId) => + String? _courseSectionId; + String? get courseSectionId => _$this._courseSectionId; + set courseSectionId(String? courseSectionId) => _$this._courseSectionId = courseSectionId; - String _enrollmentState; - String get enrollmentState => _$this._enrollmentState; - set enrollmentState(String enrollmentState) => + String? _enrollmentState; + String? get enrollmentState => _$this._enrollmentState; + set enrollmentState(String? enrollmentState) => _$this._enrollmentState = enrollmentState; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - GradeBuilder _grades; + GradeBuilder? _grades; GradeBuilder get grades => _$this._grades ??= new GradeBuilder(); - set grades(GradeBuilder grades) => _$this._grades = grades; + set grades(GradeBuilder? grades) => _$this._grades = grades; - double _computedCurrentScore; - double get computedCurrentScore => _$this._computedCurrentScore; - set computedCurrentScore(double computedCurrentScore) => + double? _computedCurrentScore; + double? get computedCurrentScore => _$this._computedCurrentScore; + set computedCurrentScore(double? computedCurrentScore) => _$this._computedCurrentScore = computedCurrentScore; - double _computedFinalScore; - double get computedFinalScore => _$this._computedFinalScore; - set computedFinalScore(double computedFinalScore) => + double? _computedFinalScore; + double? get computedFinalScore => _$this._computedFinalScore; + set computedFinalScore(double? computedFinalScore) => _$this._computedFinalScore = computedFinalScore; - String _computedCurrentGrade; - String get computedCurrentGrade => _$this._computedCurrentGrade; - set computedCurrentGrade(String computedCurrentGrade) => + String? _computedCurrentGrade; + String? get computedCurrentGrade => _$this._computedCurrentGrade; + set computedCurrentGrade(String? computedCurrentGrade) => _$this._computedCurrentGrade = computedCurrentGrade; - String _computedFinalGrade; - String get computedFinalGrade => _$this._computedFinalGrade; - set computedFinalGrade(String computedFinalGrade) => + String? _computedFinalGrade; + String? get computedFinalGrade => _$this._computedFinalGrade; + set computedFinalGrade(String? computedFinalGrade) => _$this._computedFinalGrade = computedFinalGrade; - String _computedCurrentLetterGrade; - String get computedCurrentLetterGrade => _$this._computedCurrentLetterGrade; - set computedCurrentLetterGrade(String computedCurrentLetterGrade) => + String? _computedCurrentLetterGrade; + String? get computedCurrentLetterGrade => _$this._computedCurrentLetterGrade; + set computedCurrentLetterGrade(String? computedCurrentLetterGrade) => _$this._computedCurrentLetterGrade = computedCurrentLetterGrade; - bool _multipleGradingPeriodsEnabled; - bool get multipleGradingPeriodsEnabled => + bool? _multipleGradingPeriodsEnabled; + bool? get multipleGradingPeriodsEnabled => _$this._multipleGradingPeriodsEnabled; - set multipleGradingPeriodsEnabled(bool multipleGradingPeriodsEnabled) => + set multipleGradingPeriodsEnabled(bool? multipleGradingPeriodsEnabled) => _$this._multipleGradingPeriodsEnabled = multipleGradingPeriodsEnabled; - bool _totalsForAllGradingPeriodsOption; - bool get totalsForAllGradingPeriodsOption => + bool? _totalsForAllGradingPeriodsOption; + bool? get totalsForAllGradingPeriodsOption => _$this._totalsForAllGradingPeriodsOption; - set totalsForAllGradingPeriodsOption(bool totalsForAllGradingPeriodsOption) => + set totalsForAllGradingPeriodsOption( + bool? totalsForAllGradingPeriodsOption) => _$this._totalsForAllGradingPeriodsOption = totalsForAllGradingPeriodsOption; - double _currentPeriodComputedCurrentScore; - double get currentPeriodComputedCurrentScore => + double? _currentPeriodComputedCurrentScore; + double? get currentPeriodComputedCurrentScore => _$this._currentPeriodComputedCurrentScore; set currentPeriodComputedCurrentScore( - double currentPeriodComputedCurrentScore) => + double? currentPeriodComputedCurrentScore) => _$this._currentPeriodComputedCurrentScore = currentPeriodComputedCurrentScore; - double _currentPeriodComputedFinalScore; - double get currentPeriodComputedFinalScore => + double? _currentPeriodComputedFinalScore; + double? get currentPeriodComputedFinalScore => _$this._currentPeriodComputedFinalScore; - set currentPeriodComputedFinalScore(double currentPeriodComputedFinalScore) => + set currentPeriodComputedFinalScore( + double? currentPeriodComputedFinalScore) => _$this._currentPeriodComputedFinalScore = currentPeriodComputedFinalScore; - String _currentPeriodComputedCurrentGrade; - String get currentPeriodComputedCurrentGrade => + String? _currentPeriodComputedCurrentGrade; + String? get currentPeriodComputedCurrentGrade => _$this._currentPeriodComputedCurrentGrade; set currentPeriodComputedCurrentGrade( - String currentPeriodComputedCurrentGrade) => + String? currentPeriodComputedCurrentGrade) => _$this._currentPeriodComputedCurrentGrade = currentPeriodComputedCurrentGrade; - String _currentPeriodComputedFinalGrade; - String get currentPeriodComputedFinalGrade => + String? _currentPeriodComputedFinalGrade; + String? get currentPeriodComputedFinalGrade => _$this._currentPeriodComputedFinalGrade; - set currentPeriodComputedFinalGrade(String currentPeriodComputedFinalGrade) => + set currentPeriodComputedFinalGrade( + String? currentPeriodComputedFinalGrade) => _$this._currentPeriodComputedFinalGrade = currentPeriodComputedFinalGrade; - String _currentGradingPeriodId; - String get currentGradingPeriodId => _$this._currentGradingPeriodId; - set currentGradingPeriodId(String currentGradingPeriodId) => + String? _currentGradingPeriodId; + String? get currentGradingPeriodId => _$this._currentGradingPeriodId; + set currentGradingPeriodId(String? currentGradingPeriodId) => _$this._currentGradingPeriodId = currentGradingPeriodId; - String _currentGradingPeriodTitle; - String get currentGradingPeriodTitle => _$this._currentGradingPeriodTitle; - set currentGradingPeriodTitle(String currentGradingPeriodTitle) => + String? _currentGradingPeriodTitle; + String? get currentGradingPeriodTitle => _$this._currentGradingPeriodTitle; + set currentGradingPeriodTitle(String? currentGradingPeriodTitle) => _$this._currentGradingPeriodTitle = currentGradingPeriodTitle; - String _associatedUserId; - String get associatedUserId => _$this._associatedUserId; - set associatedUserId(String associatedUserId) => + String? _associatedUserId; + String? get associatedUserId => _$this._associatedUserId; + set associatedUserId(String? associatedUserId) => _$this._associatedUserId = associatedUserId; - DateTime _lastActivityAt; - DateTime get lastActivityAt => _$this._lastActivityAt; - set lastActivityAt(DateTime lastActivityAt) => + DateTime? _lastActivityAt; + DateTime? get lastActivityAt => _$this._lastActivityAt; + set lastActivityAt(DateTime? lastActivityAt) => _$this._lastActivityAt = lastActivityAt; - bool _limitPrivilegesToCourseSection; - bool get limitPrivilegesToCourseSection => + bool? _limitPrivilegesToCourseSection; + bool? get limitPrivilegesToCourseSection => _$this._limitPrivilegesToCourseSection; - set limitPrivilegesToCourseSection(bool limitPrivilegesToCourseSection) => + set limitPrivilegesToCourseSection(bool? limitPrivilegesToCourseSection) => _$this._limitPrivilegesToCourseSection = limitPrivilegesToCourseSection; - UserBuilder _observedUser; + UserBuilder? _observedUser; UserBuilder get observedUser => _$this._observedUser ??= new UserBuilder(); - set observedUser(UserBuilder observedUser) => + set observedUser(UserBuilder? observedUser) => _$this._observedUser = observedUser; - UserBuilder _user; + UserBuilder? _user; UserBuilder get user => _$this._user ??= new UserBuilder(); - set user(UserBuilder user) => _$this._user = user; + set user(UserBuilder? user) => _$this._user = user; EnrollmentBuilder() { Enrollment._initializeBuilder(this); @@ -689,25 +683,28 @@ class EnrollmentBuilder implements Builder { } @override - void update(void Function(EnrollmentBuilder) updates) { + void update(void Function(EnrollmentBuilder)? updates) { if (updates != null) updates(this); } @override - _$Enrollment build() { + Enrollment build() => _build(); + + _$Enrollment _build() { _$Enrollment _$result; try { _$result = _$v ?? new _$Enrollment._( role: role, type: type, - id: BuiltValueNullFieldError.checkNotNull(id, 'Enrollment', 'id'), + id: BuiltValueNullFieldError.checkNotNull( + id, r'Enrollment', 'id'), courseId: courseId, courseSectionId: courseSectionId, enrollmentState: BuiltValueNullFieldError.checkNotNull( - enrollmentState, 'Enrollment', 'enrollmentState'), + enrollmentState, r'Enrollment', 'enrollmentState'), userId: BuiltValueNullFieldError.checkNotNull( - userId, 'Enrollment', 'userId'), + userId, r'Enrollment', 'userId'), grades: _grades?.build(), computedCurrentScore: computedCurrentScore, computedFinalScore: computedFinalScore, @@ -715,10 +712,10 @@ class EnrollmentBuilder implements Builder { computedFinalGrade: computedFinalGrade, computedCurrentLetterGrade: computedCurrentLetterGrade, multipleGradingPeriodsEnabled: BuiltValueNullFieldError.checkNotNull( - multipleGradingPeriodsEnabled, 'Enrollment', 'multipleGradingPeriodsEnabled'), + multipleGradingPeriodsEnabled, r'Enrollment', 'multipleGradingPeriodsEnabled'), totalsForAllGradingPeriodsOption: BuiltValueNullFieldError.checkNotNull( totalsForAllGradingPeriodsOption, - 'Enrollment', + r'Enrollment', 'totalsForAllGradingPeriodsOption'), currentPeriodComputedCurrentScore: currentPeriodComputedCurrentScore, @@ -729,16 +726,14 @@ class EnrollmentBuilder implements Builder { currentGradingPeriodId: currentGradingPeriodId, currentGradingPeriodTitle: currentGradingPeriodTitle, associatedUserId: BuiltValueNullFieldError.checkNotNull( - associatedUserId, 'Enrollment', 'associatedUserId'), + associatedUserId, r'Enrollment', 'associatedUserId'), lastActivityAt: lastActivityAt, limitPrivilegesToCourseSection: BuiltValueNullFieldError.checkNotNull( - limitPrivilegesToCourseSection, - 'Enrollment', - 'limitPrivilegesToCourseSection'), + limitPrivilegesToCourseSection, r'Enrollment', 'limitPrivilegesToCourseSection'), observedUser: _observedUser?.build(), user: _user?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'grades'; _grades?.build(); @@ -749,7 +744,7 @@ class EnrollmentBuilder implements Builder { _user?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Enrollment', _$failedField, e.toString()); + r'Enrollment', _$failedField, e.toString()); } rethrow; } @@ -758,4 +753,4 @@ class EnrollmentBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/feature_flags.g.dart b/apps/flutter_parent/lib/models/feature_flags.g.dart index a4d70398f6..eb2ab7065e 100644 --- a/apps/flutter_parent/lib/models/feature_flags.g.dart +++ b/apps/flutter_parent/lib/models/feature_flags.g.dart @@ -16,9 +16,9 @@ class _$FeatureFlagsSerializer implements StructuredSerializer { final String wireName = 'FeatureFlags'; @override - Iterable serialize(Serializers serializers, FeatureFlags object, + Iterable serialize(Serializers serializers, FeatureFlags object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'send_usage_metrics', serializers.serialize(object.sendUsageMetrics, specifiedType: const FullType(bool)), @@ -28,19 +28,20 @@ class _$FeatureFlagsSerializer implements StructuredSerializer { } @override - FeatureFlags deserialize(Serializers serializers, Iterable serialized, + FeatureFlags deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new FeatureFlagsBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'send_usage_metrics': result.sendUsageMetrics = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -53,12 +54,12 @@ class _$FeatureFlags extends FeatureFlags { @override final bool sendUsageMetrics; - factory _$FeatureFlags([void Function(FeatureFlagsBuilder) updates]) => - (new FeatureFlagsBuilder()..update(updates)).build(); + factory _$FeatureFlags([void Function(FeatureFlagsBuilder)? updates]) => + (new FeatureFlagsBuilder()..update(updates))._build(); - _$FeatureFlags._({this.sendUsageMetrics}) : super._() { + _$FeatureFlags._({required this.sendUsageMetrics}) : super._() { BuiltValueNullFieldError.checkNotNull( - sendUsageMetrics, 'FeatureFlags', 'sendUsageMetrics'); + sendUsageMetrics, r'FeatureFlags', 'sendUsageMetrics'); } @override @@ -76,12 +77,15 @@ class _$FeatureFlags extends FeatureFlags { @override int get hashCode { - return $jf($jc(0, sendUsageMetrics.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, sendUsageMetrics.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('FeatureFlags') + return (newBuiltValueToStringHelper(r'FeatureFlags') ..add('sendUsageMetrics', sendUsageMetrics)) .toString(); } @@ -89,11 +93,11 @@ class _$FeatureFlags extends FeatureFlags { class FeatureFlagsBuilder implements Builder { - _$FeatureFlags _$v; + _$FeatureFlags? _$v; - bool _sendUsageMetrics; - bool get sendUsageMetrics => _$this._sendUsageMetrics; - set sendUsageMetrics(bool sendUsageMetrics) => + bool? _sendUsageMetrics; + bool? get sendUsageMetrics => _$this._sendUsageMetrics; + set sendUsageMetrics(bool? sendUsageMetrics) => _$this._sendUsageMetrics = sendUsageMetrics; FeatureFlagsBuilder() { @@ -116,19 +120,21 @@ class FeatureFlagsBuilder } @override - void update(void Function(FeatureFlagsBuilder) updates) { + void update(void Function(FeatureFlagsBuilder)? updates) { if (updates != null) updates(this); } @override - _$FeatureFlags build() { + FeatureFlags build() => _build(); + + _$FeatureFlags _build() { final _$result = _$v ?? new _$FeatureFlags._( sendUsageMetrics: BuiltValueNullFieldError.checkNotNull( - sendUsageMetrics, 'FeatureFlags', 'sendUsageMetrics')); + sendUsageMetrics, r'FeatureFlags', 'sendUsageMetrics')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/file_upload_config.dart b/apps/flutter_parent/lib/models/file_upload_config.dart index 9b16cce121..1358d3b592 100644 --- a/apps/flutter_parent/lib/models/file_upload_config.dart +++ b/apps/flutter_parent/lib/models/file_upload_config.dart @@ -25,12 +25,10 @@ abstract class FileUploadConfig implements Built get serializer => _$fileUploadConfigSerializer; @BuiltValueField(wireName: 'upload_url') - @nullable - String get url; + String? get url; @BuiltValueField(wireName: 'upload_params') - @nullable - BuiltMap get params; + BuiltMap? get params; FileUploadConfig._(); factory FileUploadConfig([void Function(FileUploadConfigBuilder) updates]) = _$FileUploadConfig; diff --git a/apps/flutter_parent/lib/models/file_upload_config.g.dart b/apps/flutter_parent/lib/models/file_upload_config.g.dart index a6774816d3..f2f498ccc9 100644 --- a/apps/flutter_parent/lib/models/file_upload_config.g.dart +++ b/apps/flutter_parent/lib/models/file_upload_config.g.dart @@ -17,48 +17,47 @@ class _$FileUploadConfigSerializer final String wireName = 'FileUploadConfig'; @override - Iterable serialize(Serializers serializers, FileUploadConfig object, + Iterable serialize(Serializers serializers, FileUploadConfig object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - result.add('upload_url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('upload_params'); - if (object.params == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.params, + final result = []; + Object? value; + value = object.url; + + result + ..add('upload_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.params; + + result + ..add('upload_params') + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltMap, const [const FullType(String), const FullType(String)]))); - } + return result; } @override FileUploadConfig deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new FileUploadConfigBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'upload_url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'upload_params': result.params.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltMap, - const [const FullType(String), const FullType(String)]))); + const [const FullType(String), const FullType(String)]))!); break; } } @@ -69,13 +68,13 @@ class _$FileUploadConfigSerializer class _$FileUploadConfig extends FileUploadConfig { @override - final String url; + final String? url; @override - final BuiltMap params; + final BuiltMap? params; factory _$FileUploadConfig( - [void Function(FileUploadConfigBuilder) updates]) => - (new FileUploadConfigBuilder()..update(updates)).build(); + [void Function(FileUploadConfigBuilder)? updates]) => + (new FileUploadConfigBuilder()..update(updates))._build(); _$FileUploadConfig._({this.url, this.params}) : super._(); @@ -97,12 +96,16 @@ class _$FileUploadConfig extends FileUploadConfig { @override int get hashCode { - return $jf($jc($jc(0, url.hashCode), params.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, params.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('FileUploadConfig') + return (newBuiltValueToStringHelper(r'FileUploadConfig') ..add('url', url) ..add('params', params)) .toString(); @@ -111,23 +114,24 @@ class _$FileUploadConfig extends FileUploadConfig { class FileUploadConfigBuilder implements Builder { - _$FileUploadConfig _$v; + _$FileUploadConfig? _$v; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - MapBuilder _params; + MapBuilder? _params; MapBuilder get params => _$this._params ??= new MapBuilder(); - set params(MapBuilder params) => _$this._params = params; + set params(MapBuilder? params) => _$this._params = params; FileUploadConfigBuilder(); FileUploadConfigBuilder get _$this { - if (_$v != null) { - _url = _$v.url; - _params = _$v.params?.toBuilder(); + final $v = _$v; + if ($v != null) { + _url = $v.url; + _params = $v.params?.toBuilder(); _$v = null; } return this; @@ -135,31 +139,31 @@ class FileUploadConfigBuilder @override void replace(FileUploadConfig other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$FileUploadConfig; } @override - void update(void Function(FileUploadConfigBuilder) updates) { + void update(void Function(FileUploadConfigBuilder)? updates) { if (updates != null) updates(this); } @override - _$FileUploadConfig build() { + FileUploadConfig build() => _build(); + + _$FileUploadConfig _build() { _$FileUploadConfig _$result; try { _$result = _$v ?? new _$FileUploadConfig._(url: url, params: _params?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'params'; _params?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'FileUploadConfig', _$failedField, e.toString()); + r'FileUploadConfig', _$failedField, e.toString()); } rethrow; } @@ -168,4 +172,4 @@ class FileUploadConfigBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/grade.dart b/apps/flutter_parent/lib/models/grade.dart index 41bef8c9e8..707e22495b 100644 --- a/apps/flutter_parent/lib/models/grade.dart +++ b/apps/flutter_parent/lib/models/grade.dart @@ -29,18 +29,14 @@ abstract class Grade implements Built { String get htmlUrl; @BuiltValueField(wireName: 'current_score') - @nullable - double get currentScore; + double? get currentScore; @BuiltValueField(wireName: 'final_score') - @nullable - double get finalScore; + double? get finalScore; @BuiltValueField(wireName: 'current_grade') - @nullable - String get currentGrade; + String? get currentGrade; @BuiltValueField(wireName: 'final_grade') - @nullable - String get finalGrade; + String? get finalGrade; } diff --git a/apps/flutter_parent/lib/models/grade.g.dart b/apps/flutter_parent/lib/models/grade.g.dart index 95662459c7..82caf078bc 100644 --- a/apps/flutter_parent/lib/models/grade.g.dart +++ b/apps/flutter_parent/lib/models/grade.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of grade; +part of 'grade.dart'; // ************************************************************************** // BuiltValueGenerator @@ -15,75 +15,72 @@ class _$GradeSerializer implements StructuredSerializer { final String wireName = 'Grade'; @override - Iterable serialize(Serializers serializers, Grade object, + Iterable serialize(Serializers serializers, Grade object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'html_url', serializers.serialize(object.htmlUrl, specifiedType: const FullType(String)), ]; - result.add('current_score'); - if (object.currentScore == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.currentScore, - specifiedType: const FullType(double))); - } - result.add('final_score'); - if (object.finalScore == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.finalScore, - specifiedType: const FullType(double))); - } - result.add('current_grade'); - if (object.currentGrade == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.currentGrade, - specifiedType: const FullType(String))); - } - result.add('final_grade'); - if (object.finalGrade == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.finalGrade, - specifiedType: const FullType(String))); - } + Object? value; + value = object.currentScore; + + result + ..add('current_score') + ..add( + serializers.serialize(value, specifiedType: const FullType(double))); + value = object.finalScore; + + result + ..add('final_score') + ..add( + serializers.serialize(value, specifiedType: const FullType(double))); + value = object.currentGrade; + + result + ..add('current_grade') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.finalGrade; + + result + ..add('final_grade') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - Grade deserialize(Serializers serializers, Iterable serialized, + Grade deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new GradeBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'current_score': result.currentScore = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'final_score': result.finalScore = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'current_grade': result.currentGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'final_grade': result.finalGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -96,27 +93,25 @@ class _$Grade extends Grade { @override final String htmlUrl; @override - final double currentScore; + final double? currentScore; @override - final double finalScore; + final double? finalScore; @override - final String currentGrade; + final String? currentGrade; @override - final String finalGrade; + final String? finalGrade; - factory _$Grade([void Function(GradeBuilder) updates]) => - (new GradeBuilder()..update(updates)).build(); + factory _$Grade([void Function(GradeBuilder)? updates]) => + (new GradeBuilder()..update(updates))._build(); _$Grade._( - {this.htmlUrl, + {required this.htmlUrl, this.currentScore, this.finalScore, this.currentGrade, this.finalGrade}) : super._() { - if (htmlUrl == null) { - throw new BuiltValueNullFieldError('Grade', 'htmlUrl'); - } + BuiltValueNullFieldError.checkNotNull(htmlUrl, r'Grade', 'htmlUrl'); } @override @@ -139,17 +134,19 @@ class _$Grade extends Grade { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc(0, htmlUrl.hashCode), currentScore.hashCode), - finalScore.hashCode), - currentGrade.hashCode), - finalGrade.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, currentScore.hashCode); + _$hash = $jc(_$hash, finalScore.hashCode); + _$hash = $jc(_$hash, currentGrade.hashCode); + _$hash = $jc(_$hash, finalGrade.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Grade') + return (newBuiltValueToStringHelper(r'Grade') ..add('htmlUrl', htmlUrl) ..add('currentScore', currentScore) ..add('finalScore', finalScore) @@ -160,37 +157,38 @@ class _$Grade extends Grade { } class GradeBuilder implements Builder { - _$Grade _$v; + _$Grade? _$v; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - double _currentScore; - double get currentScore => _$this._currentScore; - set currentScore(double currentScore) => _$this._currentScore = currentScore; + double? _currentScore; + double? get currentScore => _$this._currentScore; + set currentScore(double? currentScore) => _$this._currentScore = currentScore; - double _finalScore; - double get finalScore => _$this._finalScore; - set finalScore(double finalScore) => _$this._finalScore = finalScore; + double? _finalScore; + double? get finalScore => _$this._finalScore; + set finalScore(double? finalScore) => _$this._finalScore = finalScore; - String _currentGrade; - String get currentGrade => _$this._currentGrade; - set currentGrade(String currentGrade) => _$this._currentGrade = currentGrade; + String? _currentGrade; + String? get currentGrade => _$this._currentGrade; + set currentGrade(String? currentGrade) => _$this._currentGrade = currentGrade; - String _finalGrade; - String get finalGrade => _$this._finalGrade; - set finalGrade(String finalGrade) => _$this._finalGrade = finalGrade; + String? _finalGrade; + String? get finalGrade => _$this._finalGrade; + set finalGrade(String? finalGrade) => _$this._finalGrade = finalGrade; GradeBuilder(); GradeBuilder get _$this { - if (_$v != null) { - _htmlUrl = _$v.htmlUrl; - _currentScore = _$v.currentScore; - _finalScore = _$v.finalScore; - _currentGrade = _$v.currentGrade; - _finalGrade = _$v.finalGrade; + final $v = _$v; + if ($v != null) { + _htmlUrl = $v.htmlUrl; + _currentScore = $v.currentScore; + _finalScore = $v.finalScore; + _currentGrade = $v.currentGrade; + _finalGrade = $v.finalGrade; _$v = null; } return this; @@ -198,22 +196,23 @@ class GradeBuilder implements Builder { @override void replace(Grade other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Grade; } @override - void update(void Function(GradeBuilder) updates) { + void update(void Function(GradeBuilder)? updates) { if (updates != null) updates(this); } @override - _$Grade build() { + Grade build() => _build(); + + _$Grade _build() { final _$result = _$v ?? new _$Grade._( - htmlUrl: htmlUrl, + htmlUrl: BuiltValueNullFieldError.checkNotNull( + htmlUrl, r'Grade', 'htmlUrl'), currentScore: currentScore, finalScore: finalScore, currentGrade: currentGrade, @@ -223,4 +222,4 @@ class GradeBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/grade_cell_data.dart b/apps/flutter_parent/lib/models/grade_cell_data.dart index b9c0d5d11f..642c29700e 100644 --- a/apps/flutter_parent/lib/models/grade_cell_data.dart +++ b/apps/flutter_parent/lib/models/grade_cell_data.dart @@ -62,9 +62,9 @@ abstract class GradeCellData implements Built b ..state = GradeCellState.submitted - ..submissionText = submission.submittedAt.l10nFormat( + ..submissionText = submission.submittedAt!.l10nFormat( l10n.submissionStatusSuccessSubtitle, dateFormat: DateFormat.MMMMd(supportedDateLocale), )); } - var accentColor = theme.accentColor; + var accentColor = theme.colorScheme.secondary; var pointsPossibleText = NumberFormat.decimalPattern().format(assignment.pointsPossible); @@ -132,7 +132,7 @@ abstract class GradeCellData implements Built - (new GradeCellDataBuilder()..update(updates)).build(); + factory _$GradeCellData([void Function(GradeCellDataBuilder)? updates]) => + (new GradeCellDataBuilder()..update(updates))._build(); _$GradeCellData._( - {this.state, - this.submissionText, - this.showCompleteIcon, - this.showIncompleteIcon, - this.showPointsLabel, - this.accentColor, - this.graphPercent, - this.score, - this.grade, - this.gradeContentDescription, - this.outOf, - this.latePenalty, - this.finalGrade}) + {required this.state, + required this.submissionText, + required this.showCompleteIcon, + required this.showIncompleteIcon, + required this.showPointsLabel, + required this.accentColor, + required this.graphPercent, + required this.score, + required this.grade, + required this.gradeContentDescription, + required this.outOf, + required this.latePenalty, + required this.finalGrade}) : super._() { - if (state == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'state'); - } - if (submissionText == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'submissionText'); - } - if (showCompleteIcon == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'showCompleteIcon'); - } - if (showIncompleteIcon == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'showIncompleteIcon'); - } - if (showPointsLabel == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'showPointsLabel'); - } - if (accentColor == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'accentColor'); - } - if (graphPercent == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'graphPercent'); - } - if (score == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'score'); - } - if (grade == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'grade'); - } - if (gradeContentDescription == null) { - throw new BuiltValueNullFieldError( - 'GradeCellData', 'gradeContentDescription'); - } - if (outOf == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'outOf'); - } - if (latePenalty == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'latePenalty'); - } - if (finalGrade == null) { - throw new BuiltValueNullFieldError('GradeCellData', 'finalGrade'); - } + BuiltValueNullFieldError.checkNotNull(state, r'GradeCellData', 'state'); + BuiltValueNullFieldError.checkNotNull( + submissionText, r'GradeCellData', 'submissionText'); + BuiltValueNullFieldError.checkNotNull( + showCompleteIcon, r'GradeCellData', 'showCompleteIcon'); + BuiltValueNullFieldError.checkNotNull( + showIncompleteIcon, r'GradeCellData', 'showIncompleteIcon'); + BuiltValueNullFieldError.checkNotNull( + showPointsLabel, r'GradeCellData', 'showPointsLabel'); + BuiltValueNullFieldError.checkNotNull( + accentColor, r'GradeCellData', 'accentColor'); + BuiltValueNullFieldError.checkNotNull( + graphPercent, r'GradeCellData', 'graphPercent'); + BuiltValueNullFieldError.checkNotNull(score, r'GradeCellData', 'score'); + BuiltValueNullFieldError.checkNotNull(grade, r'GradeCellData', 'grade'); + BuiltValueNullFieldError.checkNotNull( + gradeContentDescription, r'GradeCellData', 'gradeContentDescription'); + BuiltValueNullFieldError.checkNotNull(outOf, r'GradeCellData', 'outOf'); + BuiltValueNullFieldError.checkNotNull( + latePenalty, r'GradeCellData', 'latePenalty'); + BuiltValueNullFieldError.checkNotNull( + finalGrade, r'GradeCellData', 'finalGrade'); } @override @@ -122,35 +104,27 @@ class _$GradeCellData extends GradeCellData { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc(0, state.hashCode), - submissionText.hashCode), - showCompleteIcon.hashCode), - showIncompleteIcon.hashCode), - showPointsLabel.hashCode), - accentColor.hashCode), - graphPercent.hashCode), - score.hashCode), - grade.hashCode), - gradeContentDescription.hashCode), - outOf.hashCode), - latePenalty.hashCode), - finalGrade.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, state.hashCode); + _$hash = $jc(_$hash, submissionText.hashCode); + _$hash = $jc(_$hash, showCompleteIcon.hashCode); + _$hash = $jc(_$hash, showIncompleteIcon.hashCode); + _$hash = $jc(_$hash, showPointsLabel.hashCode); + _$hash = $jc(_$hash, accentColor.hashCode); + _$hash = $jc(_$hash, graphPercent.hashCode); + _$hash = $jc(_$hash, score.hashCode); + _$hash = $jc(_$hash, grade.hashCode); + _$hash = $jc(_$hash, gradeContentDescription.hashCode); + _$hash = $jc(_$hash, outOf.hashCode); + _$hash = $jc(_$hash, latePenalty.hashCode); + _$hash = $jc(_$hash, finalGrade.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('GradeCellData') + return (newBuiltValueToStringHelper(r'GradeCellData') ..add('state', state) ..add('submissionText', submissionText) ..add('showCompleteIcon', showCompleteIcon) @@ -170,84 +144,85 @@ class _$GradeCellData extends GradeCellData { class GradeCellDataBuilder implements Builder { - _$GradeCellData _$v; + _$GradeCellData? _$v; - GradeCellState _state; - GradeCellState get state => _$this._state; - set state(GradeCellState state) => _$this._state = state; + GradeCellState? _state; + GradeCellState? get state => _$this._state; + set state(GradeCellState? state) => _$this._state = state; - String _submissionText; - String get submissionText => _$this._submissionText; - set submissionText(String submissionText) => + String? _submissionText; + String? get submissionText => _$this._submissionText; + set submissionText(String? submissionText) => _$this._submissionText = submissionText; - bool _showCompleteIcon; - bool get showCompleteIcon => _$this._showCompleteIcon; - set showCompleteIcon(bool showCompleteIcon) => + bool? _showCompleteIcon; + bool? get showCompleteIcon => _$this._showCompleteIcon; + set showCompleteIcon(bool? showCompleteIcon) => _$this._showCompleteIcon = showCompleteIcon; - bool _showIncompleteIcon; - bool get showIncompleteIcon => _$this._showIncompleteIcon; - set showIncompleteIcon(bool showIncompleteIcon) => + bool? _showIncompleteIcon; + bool? get showIncompleteIcon => _$this._showIncompleteIcon; + set showIncompleteIcon(bool? showIncompleteIcon) => _$this._showIncompleteIcon = showIncompleteIcon; - bool _showPointsLabel; - bool get showPointsLabel => _$this._showPointsLabel; - set showPointsLabel(bool showPointsLabel) => + bool? _showPointsLabel; + bool? get showPointsLabel => _$this._showPointsLabel; + set showPointsLabel(bool? showPointsLabel) => _$this._showPointsLabel = showPointsLabel; - Color _accentColor; - Color get accentColor => _$this._accentColor; - set accentColor(Color accentColor) => _$this._accentColor = accentColor; + Color? _accentColor; + Color? get accentColor => _$this._accentColor; + set accentColor(Color? accentColor) => _$this._accentColor = accentColor; - double _graphPercent; - double get graphPercent => _$this._graphPercent; - set graphPercent(double graphPercent) => _$this._graphPercent = graphPercent; + double? _graphPercent; + double? get graphPercent => _$this._graphPercent; + set graphPercent(double? graphPercent) => _$this._graphPercent = graphPercent; - String _score; - String get score => _$this._score; - set score(String score) => _$this._score = score; + String? _score; + String? get score => _$this._score; + set score(String? score) => _$this._score = score; - String _grade; - String get grade => _$this._grade; - set grade(String grade) => _$this._grade = grade; + String? _grade; + String? get grade => _$this._grade; + set grade(String? grade) => _$this._grade = grade; - String _gradeContentDescription; - String get gradeContentDescription => _$this._gradeContentDescription; - set gradeContentDescription(String gradeContentDescription) => + String? _gradeContentDescription; + String? get gradeContentDescription => _$this._gradeContentDescription; + set gradeContentDescription(String? gradeContentDescription) => _$this._gradeContentDescription = gradeContentDescription; - String _outOf; - String get outOf => _$this._outOf; - set outOf(String outOf) => _$this._outOf = outOf; + String? _outOf; + String? get outOf => _$this._outOf; + set outOf(String? outOf) => _$this._outOf = outOf; - String _latePenalty; - String get latePenalty => _$this._latePenalty; - set latePenalty(String latePenalty) => _$this._latePenalty = latePenalty; + String? _latePenalty; + String? get latePenalty => _$this._latePenalty; + set latePenalty(String? latePenalty) => _$this._latePenalty = latePenalty; - String _finalGrade; - String get finalGrade => _$this._finalGrade; - set finalGrade(String finalGrade) => _$this._finalGrade = finalGrade; + String? _finalGrade; + String? get finalGrade => _$this._finalGrade; + set finalGrade(String? finalGrade) => _$this._finalGrade = finalGrade; GradeCellDataBuilder() { GradeCellData._initializeBuilder(this); } GradeCellDataBuilder get _$this { - if (_$v != null) { - _state = _$v.state; - _submissionText = _$v.submissionText; - _showCompleteIcon = _$v.showCompleteIcon; - _showIncompleteIcon = _$v.showIncompleteIcon; - _showPointsLabel = _$v.showPointsLabel; - _accentColor = _$v.accentColor; - _graphPercent = _$v.graphPercent; - _score = _$v.score; - _grade = _$v.grade; - _gradeContentDescription = _$v.gradeContentDescription; - _outOf = _$v.outOf; - _latePenalty = _$v.latePenalty; - _finalGrade = _$v.finalGrade; + final $v = _$v; + if ($v != null) { + _state = $v.state; + _submissionText = $v.submissionText; + _showCompleteIcon = $v.showCompleteIcon; + _showIncompleteIcon = $v.showIncompleteIcon; + _showPointsLabel = $v.showPointsLabel; + _accentColor = $v.accentColor; + _graphPercent = $v.graphPercent; + _score = $v.score; + _grade = $v.grade; + _gradeContentDescription = $v.gradeContentDescription; + _outOf = $v.outOf; + _latePenalty = $v.latePenalty; + _finalGrade = $v.finalGrade; _$v = null; } return this; @@ -255,37 +230,45 @@ class GradeCellDataBuilder @override void replace(GradeCellData other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$GradeCellData; } @override - void update(void Function(GradeCellDataBuilder) updates) { + void update(void Function(GradeCellDataBuilder)? updates) { if (updates != null) updates(this); } @override - _$GradeCellData build() { + GradeCellData build() => _build(); + + _$GradeCellData _build() { final _$result = _$v ?? new _$GradeCellData._( - state: state, - submissionText: submissionText, - showCompleteIcon: showCompleteIcon, - showIncompleteIcon: showIncompleteIcon, - showPointsLabel: showPointsLabel, - accentColor: accentColor, - graphPercent: graphPercent, - score: score, - grade: grade, - gradeContentDescription: gradeContentDescription, - outOf: outOf, - latePenalty: latePenalty, - finalGrade: finalGrade); + state: BuiltValueNullFieldError.checkNotNull( + state, r'GradeCellData', 'state'), + submissionText: BuiltValueNullFieldError.checkNotNull( + submissionText, r'GradeCellData', 'submissionText'), + showCompleteIcon: BuiltValueNullFieldError.checkNotNull( + showCompleteIcon, r'GradeCellData', 'showCompleteIcon'), + showIncompleteIcon: BuiltValueNullFieldError.checkNotNull( + showIncompleteIcon, r'GradeCellData', 'showIncompleteIcon'), + showPointsLabel: BuiltValueNullFieldError.checkNotNull( + showPointsLabel, r'GradeCellData', 'showPointsLabel'), + accentColor: BuiltValueNullFieldError.checkNotNull( + accentColor, r'GradeCellData', 'accentColor'), + graphPercent: BuiltValueNullFieldError.checkNotNull( + graphPercent, r'GradeCellData', 'graphPercent'), + score: BuiltValueNullFieldError.checkNotNull( + score, r'GradeCellData', 'score'), + grade: BuiltValueNullFieldError.checkNotNull(grade, r'GradeCellData', 'grade'), + gradeContentDescription: BuiltValueNullFieldError.checkNotNull(gradeContentDescription, r'GradeCellData', 'gradeContentDescription'), + outOf: BuiltValueNullFieldError.checkNotNull(outOf, r'GradeCellData', 'outOf'), + latePenalty: BuiltValueNullFieldError.checkNotNull(latePenalty, r'GradeCellData', 'latePenalty'), + finalGrade: BuiltValueNullFieldError.checkNotNull(finalGrade, r'GradeCellData', 'finalGrade')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/grading_period.dart b/apps/flutter_parent/lib/models/grading_period.dart index 164eac575e..7c0a160dd0 100644 --- a/apps/flutter_parent/lib/models/grading_period.dart +++ b/apps/flutter_parent/lib/models/grading_period.dart @@ -26,20 +26,15 @@ abstract class GradingPeriod implements Built { final String wireName = 'GradingPeriod'; @override - Iterable serialize(Serializers serializers, GradingPeriod object, + Iterable serialize(Serializers serializers, GradingPeriod object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - result.add('id'); - if (object.id == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.id, - specifiedType: const FullType(String))); - } - result.add('title'); - if (object.title == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.title, - specifiedType: const FullType(String))); - } - result.add('start_date'); - if (object.startDate == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.startDate, + final result = []; + Object? value; + value = object.id; + + result + ..add('id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.title; + + result + ..add('title') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.startDate; + + result + ..add('start_date') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('end_date'); - if (object.endDate == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.endDate, + value = object.endDate; + + result + ..add('end_date') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('weight'); - if (object.weight == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.weight, - specifiedType: const FullType(double))); - } + value = object.weight; + + result + ..add('weight') + ..add( + serializers.serialize(value, specifiedType: const FullType(double))); + return result; } @override GradingPeriod deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new GradingPeriodBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'start_date': result.startDate = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'end_date': result.endDate = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'weight': result.weight = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; } } @@ -99,18 +95,18 @@ class _$GradingPeriodSerializer implements StructuredSerializer { class _$GradingPeriod extends GradingPeriod { @override - final String id; + final String? id; @override - final String title; + final String? title; @override - final DateTime startDate; + final DateTime? startDate; @override - final DateTime endDate; + final DateTime? endDate; @override - final double weight; + final double? weight; - factory _$GradingPeriod([void Function(GradingPeriodBuilder) updates]) => - (new GradingPeriodBuilder()..update(updates)).build(); + factory _$GradingPeriod([void Function(GradingPeriodBuilder)? updates]) => + (new GradingPeriodBuilder()..update(updates))._build(); _$GradingPeriod._( {this.id, this.title, this.startDate, this.endDate, this.weight}) @@ -136,15 +132,19 @@ class _$GradingPeriod extends GradingPeriod { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), title.hashCode), startDate.hashCode), - endDate.hashCode), - weight.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, startDate.hashCode); + _$hash = $jc(_$hash, endDate.hashCode); + _$hash = $jc(_$hash, weight.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('GradingPeriod') + return (newBuiltValueToStringHelper(r'GradingPeriod') ..add('id', id) ..add('title', title) ..add('startDate', startDate) @@ -156,37 +156,38 @@ class _$GradingPeriod extends GradingPeriod { class GradingPeriodBuilder implements Builder { - _$GradingPeriod _$v; + _$GradingPeriod? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - DateTime _startDate; - DateTime get startDate => _$this._startDate; - set startDate(DateTime startDate) => _$this._startDate = startDate; + DateTime? _startDate; + DateTime? get startDate => _$this._startDate; + set startDate(DateTime? startDate) => _$this._startDate = startDate; - DateTime _endDate; - DateTime get endDate => _$this._endDate; - set endDate(DateTime endDate) => _$this._endDate = endDate; + DateTime? _endDate; + DateTime? get endDate => _$this._endDate; + set endDate(DateTime? endDate) => _$this._endDate = endDate; - double _weight; - double get weight => _$this._weight; - set weight(double weight) => _$this._weight = weight; + double? _weight; + double? get weight => _$this._weight; + set weight(double? weight) => _$this._weight = weight; GradingPeriodBuilder(); GradingPeriodBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _title = _$v.title; - _startDate = _$v.startDate; - _endDate = _$v.endDate; - _weight = _$v.weight; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _title = $v.title; + _startDate = $v.startDate; + _endDate = $v.endDate; + _weight = $v.weight; _$v = null; } return this; @@ -194,19 +195,19 @@ class GradingPeriodBuilder @override void replace(GradingPeriod other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$GradingPeriod; } @override - void update(void Function(GradingPeriodBuilder) updates) { + void update(void Function(GradingPeriodBuilder)? updates) { if (updates != null) updates(this); } @override - _$GradingPeriod build() { + GradingPeriod build() => _build(); + + _$GradingPeriod _build() { final _$result = _$v ?? new _$GradingPeriod._( id: id, @@ -219,4 +220,4 @@ class GradingPeriodBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/grading_period_response.g.dart b/apps/flutter_parent/lib/models/grading_period_response.g.dart index df6e61d6b4..d68204168e 100644 --- a/apps/flutter_parent/lib/models/grading_period_response.g.dart +++ b/apps/flutter_parent/lib/models/grading_period_response.g.dart @@ -20,10 +20,10 @@ class _$GradingPeriodResponseSerializer final String wireName = 'GradingPeriodResponse'; @override - Iterable serialize( + Iterable serialize( Serializers serializers, GradingPeriodResponse object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'grading_periods', serializers.serialize(object.gradingPeriods, specifiedType: @@ -35,22 +35,21 @@ class _$GradingPeriodResponseSerializer @override GradingPeriodResponse deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new GradingPeriodResponseBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'grading_periods': result.gradingPeriods.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(GradingPeriod)])) - as BuiltList); + BuiltList, const [const FullType(GradingPeriod)]))! + as BuiltList); break; } } @@ -64,14 +63,12 @@ class _$GradingPeriodResponse extends GradingPeriodResponse { final BuiltList gradingPeriods; factory _$GradingPeriodResponse( - [void Function(GradingPeriodResponseBuilder) updates]) => - (new GradingPeriodResponseBuilder()..update(updates)).build(); + [void Function(GradingPeriodResponseBuilder)? updates]) => + (new GradingPeriodResponseBuilder()..update(updates))._build(); - _$GradingPeriodResponse._({this.gradingPeriods}) : super._() { - if (gradingPeriods == null) { - throw new BuiltValueNullFieldError( - 'GradingPeriodResponse', 'gradingPeriods'); - } + _$GradingPeriodResponse._({required this.gradingPeriods}) : super._() { + BuiltValueNullFieldError.checkNotNull( + gradingPeriods, r'GradingPeriodResponse', 'gradingPeriods'); } @override @@ -92,12 +89,15 @@ class _$GradingPeriodResponse extends GradingPeriodResponse { @override int get hashCode { - return $jf($jc(0, gradingPeriods.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, gradingPeriods.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('GradingPeriodResponse') + return (newBuiltValueToStringHelper(r'GradingPeriodResponse') ..add('gradingPeriods', gradingPeriods)) .toString(); } @@ -105,19 +105,20 @@ class _$GradingPeriodResponse extends GradingPeriodResponse { class GradingPeriodResponseBuilder implements Builder { - _$GradingPeriodResponse _$v; + _$GradingPeriodResponse? _$v; - ListBuilder _gradingPeriods; + ListBuilder? _gradingPeriods; ListBuilder get gradingPeriods => _$this._gradingPeriods ??= new ListBuilder(); - set gradingPeriods(ListBuilder gradingPeriods) => + set gradingPeriods(ListBuilder? gradingPeriods) => _$this._gradingPeriods = gradingPeriods; GradingPeriodResponseBuilder(); GradingPeriodResponseBuilder get _$this { - if (_$v != null) { - _gradingPeriods = _$v.gradingPeriods?.toBuilder(); + final $v = _$v; + if ($v != null) { + _gradingPeriods = $v.gradingPeriods.toBuilder(); _$v = null; } return this; @@ -125,31 +126,31 @@ class GradingPeriodResponseBuilder @override void replace(GradingPeriodResponse other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$GradingPeriodResponse; } @override - void update(void Function(GradingPeriodResponseBuilder) updates) { + void update(void Function(GradingPeriodResponseBuilder)? updates) { if (updates != null) updates(this); } @override - _$GradingPeriodResponse build() { + GradingPeriodResponse build() => _build(); + + _$GradingPeriodResponse _build() { _$GradingPeriodResponse _$result; try { _$result = _$v ?? new _$GradingPeriodResponse._(gradingPeriods: gradingPeriods.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'gradingPeriods'; gradingPeriods.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'GradingPeriodResponse', _$failedField, e.toString()); + r'GradingPeriodResponse', _$failedField, e.toString()); } rethrow; } @@ -158,4 +159,4 @@ class GradingPeriodResponseBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/grading_scheme_item.dart b/apps/flutter_parent/lib/models/grading_scheme_item.dart index 64010dda6b..477f8508d8 100644 --- a/apps/flutter_parent/lib/models/grading_scheme_item.dart +++ b/apps/flutter_parent/lib/models/grading_scheme_item.dart @@ -28,9 +28,12 @@ abstract class GradingSchemeItem implements Built b + ..grade = null + ..value = null); + if (!json.isList) return emptyGradingSchemeItem; List items = json.asList; - if (!(items[0] is String) || !(items[1] is num)) return null; + if (!(items[0] is String) || !(items[1] is num)) return emptyGradingSchemeItem; String grade = items[0] as String; double value = (items[1] as num).toDouble(); return GradingSchemeItem((b) => b @@ -38,7 +41,7 @@ abstract class GradingSchemeItem implements Built serialize(Serializers serializers, GradingSchemeItem object, + Iterable serialize(Serializers serializers, GradingSchemeItem object, {FullType specifiedType = FullType.unspecified}) { - final result = [ - 'grade', - serializers.serialize(object.grade, - specifiedType: const FullType(String)), - 'value', - serializers.serialize(object.value, - specifiedType: const FullType(double)), - ]; - + final result = []; + Object? value; + value = object.grade; + if (value != null) { + result + ..add('grade') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + value = object.value; + if (value != null) { + result + ..add('value') + ..add(serializers.serialize(value, + specifiedType: const FullType(double))); + } return result; } @override GradingSchemeItem deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new GradingSchemeItemBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'grade': result.grade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'value': result.value = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; } } @@ -60,18 +67,15 @@ class _$GradingSchemeItemSerializer class _$GradingSchemeItem extends GradingSchemeItem { @override - final String grade; + final String? grade; @override - final double value; + final double? value; factory _$GradingSchemeItem( - [void Function(GradingSchemeItemBuilder) updates]) => - (new GradingSchemeItemBuilder()..update(updates)).build(); + [void Function(GradingSchemeItemBuilder)? updates]) => + (new GradingSchemeItemBuilder()..update(updates))._build(); - _$GradingSchemeItem._({this.grade, this.value}) : super._() { - BuiltValueNullFieldError.checkNotNull(grade, 'GradingSchemeItem', 'grade'); - BuiltValueNullFieldError.checkNotNull(value, 'GradingSchemeItem', 'value'); - } + _$GradingSchemeItem._({this.grade, this.value}) : super._(); @override GradingSchemeItem rebuild(void Function(GradingSchemeItemBuilder) updates) => @@ -91,12 +95,16 @@ class _$GradingSchemeItem extends GradingSchemeItem { @override int get hashCode { - return $jf($jc($jc(0, grade.hashCode), value.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, grade.hashCode); + _$hash = $jc(_$hash, value.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('GradingSchemeItem') + return (newBuiltValueToStringHelper(r'GradingSchemeItem') ..add('grade', grade) ..add('value', value)) .toString(); @@ -105,15 +113,15 @@ class _$GradingSchemeItem extends GradingSchemeItem { class GradingSchemeItemBuilder implements Builder { - _$GradingSchemeItem _$v; + _$GradingSchemeItem? _$v; - String _grade; - String get grade => _$this._grade; - set grade(String grade) => _$this._grade = grade; + String? _grade; + String? get grade => _$this._grade; + set grade(String? grade) => _$this._grade = grade; - double _value; - double get value => _$this._value; - set value(double value) => _$this._value = value; + double? _value; + double? get value => _$this._value; + set value(double? value) => _$this._value = value; GradingSchemeItemBuilder(); @@ -134,21 +142,19 @@ class GradingSchemeItemBuilder } @override - void update(void Function(GradingSchemeItemBuilder) updates) { + void update(void Function(GradingSchemeItemBuilder)? updates) { if (updates != null) updates(this); } @override - _$GradingSchemeItem build() { - final _$result = _$v ?? - new _$GradingSchemeItem._( - grade: BuiltValueNullFieldError.checkNotNull( - grade, 'GradingSchemeItem', 'grade'), - value: BuiltValueNullFieldError.checkNotNull( - value, 'GradingSchemeItem', 'value')); + GradingSchemeItem build() => _build(); + + _$GradingSchemeItem _build() { + final _$result = + _$v ?? new _$GradingSchemeItem._(grade: grade, value: value); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/help_link.g.dart b/apps/flutter_parent/lib/models/help_link.g.dart index ce776e79c0..a596b2cb39 100644 --- a/apps/flutter_parent/lib/models/help_link.g.dart +++ b/apps/flutter_parent/lib/models/help_link.g.dart @@ -52,9 +52,9 @@ class _$HelpLinkSerializer implements StructuredSerializer { final String wireName = 'HelpLink'; @override - Iterable serialize(Serializers serializers, HelpLink object, + Iterable serialize(Serializers serializers, HelpLink object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'type', @@ -76,42 +76,41 @@ class _$HelpLinkSerializer implements StructuredSerializer { } @override - HelpLink deserialize(Serializers serializers, Iterable serialized, + HelpLink deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new HelpLinkBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'available_to': result.availableTo.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(AvailableTo)])) - as BuiltList); + BuiltList, const [const FullType(AvailableTo)]))! + as BuiltList); break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'text': result.text = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'subtext': result.subtext = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -151,30 +150,24 @@ class _$HelpLink extends HelpLink { @override final String subtext; - factory _$HelpLink([void Function(HelpLinkBuilder) updates]) => - (new HelpLinkBuilder()..update(updates)).build(); + factory _$HelpLink([void Function(HelpLinkBuilder)? updates]) => + (new HelpLinkBuilder()..update(updates))._build(); _$HelpLink._( - {this.id, this.type, this.availableTo, this.url, this.text, this.subtext}) + {required this.id, + required this.type, + required this.availableTo, + required this.url, + required this.text, + required this.subtext}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('HelpLink', 'id'); - } - if (type == null) { - throw new BuiltValueNullFieldError('HelpLink', 'type'); - } - if (availableTo == null) { - throw new BuiltValueNullFieldError('HelpLink', 'availableTo'); - } - if (url == null) { - throw new BuiltValueNullFieldError('HelpLink', 'url'); - } - if (text == null) { - throw new BuiltValueNullFieldError('HelpLink', 'text'); - } - if (subtext == null) { - throw new BuiltValueNullFieldError('HelpLink', 'subtext'); - } + BuiltValueNullFieldError.checkNotNull(id, r'HelpLink', 'id'); + BuiltValueNullFieldError.checkNotNull(type, r'HelpLink', 'type'); + BuiltValueNullFieldError.checkNotNull( + availableTo, r'HelpLink', 'availableTo'); + BuiltValueNullFieldError.checkNotNull(url, r'HelpLink', 'url'); + BuiltValueNullFieldError.checkNotNull(text, r'HelpLink', 'text'); + BuiltValueNullFieldError.checkNotNull(subtext, r'HelpLink', 'subtext'); } @override @@ -198,19 +191,20 @@ class _$HelpLink extends HelpLink { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), type.hashCode), - availableTo.hashCode), - url.hashCode), - text.hashCode), - subtext.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, availableTo.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, text.hashCode); + _$hash = $jc(_$hash, subtext.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('HelpLink') + return (newBuiltValueToStringHelper(r'HelpLink') ..add('id', id) ..add('type', type) ..add('availableTo', availableTo) @@ -222,44 +216,45 @@ class _$HelpLink extends HelpLink { } class HelpLinkBuilder implements Builder { - _$HelpLink _$v; + _$HelpLink? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _type; - String get type => _$this._type; - set type(String type) => _$this._type = type; + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; - ListBuilder _availableTo; + ListBuilder? _availableTo; ListBuilder get availableTo => _$this._availableTo ??= new ListBuilder(); - set availableTo(ListBuilder availableTo) => + set availableTo(ListBuilder? availableTo) => _$this._availableTo = availableTo; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - String _text; - String get text => _$this._text; - set text(String text) => _$this._text = text; + String? _text; + String? get text => _$this._text; + set text(String? text) => _$this._text = text; - String _subtext; - String get subtext => _$this._subtext; - set subtext(String subtext) => _$this._subtext = subtext; + String? _subtext; + String? get subtext => _$this._subtext; + set subtext(String? subtext) => _$this._subtext = subtext; HelpLinkBuilder(); HelpLinkBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _type = _$v.type; - _availableTo = _$v.availableTo?.toBuilder(); - _url = _$v.url; - _text = _$v.text; - _subtext = _$v.subtext; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _type = $v.type; + _availableTo = $v.availableTo.toBuilder(); + _url = $v.url; + _text = $v.text; + _subtext = $v.subtext; _$v = null; } return this; @@ -267,37 +262,41 @@ class HelpLinkBuilder implements Builder { @override void replace(HelpLink other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$HelpLink; } @override - void update(void Function(HelpLinkBuilder) updates) { + void update(void Function(HelpLinkBuilder)? updates) { if (updates != null) updates(this); } @override - _$HelpLink build() { + HelpLink build() => _build(); + + _$HelpLink _build() { _$HelpLink _$result; try { _$result = _$v ?? new _$HelpLink._( - id: id, - type: type, + id: BuiltValueNullFieldError.checkNotNull(id, r'HelpLink', 'id'), + type: BuiltValueNullFieldError.checkNotNull( + type, r'HelpLink', 'type'), availableTo: availableTo.build(), - url: url, - text: text, - subtext: subtext); + url: BuiltValueNullFieldError.checkNotNull( + url, r'HelpLink', 'url'), + text: BuiltValueNullFieldError.checkNotNull( + text, r'HelpLink', 'text'), + subtext: BuiltValueNullFieldError.checkNotNull( + subtext, r'HelpLink', 'subtext')); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'availableTo'; availableTo.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'HelpLink', _$failedField, e.toString()); + r'HelpLink', _$failedField, e.toString()); } rethrow; } @@ -306,4 +305,4 @@ class HelpLinkBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/help_links.g.dart b/apps/flutter_parent/lib/models/help_links.g.dart index 0b7b03d8ad..4f4fe7bbf5 100644 --- a/apps/flutter_parent/lib/models/help_links.g.dart +++ b/apps/flutter_parent/lib/models/help_links.g.dart @@ -15,9 +15,9 @@ class _$HelpLinksSerializer implements StructuredSerializer { final String wireName = 'HelpLinks'; @override - Iterable serialize(Serializers serializers, HelpLinks object, + Iterable serialize(Serializers serializers, HelpLinks object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'custom_help_links', serializers.serialize(object.customHelpLinks, specifiedType: @@ -32,28 +32,27 @@ class _$HelpLinksSerializer implements StructuredSerializer { } @override - HelpLinks deserialize(Serializers serializers, Iterable serialized, + HelpLinks deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new HelpLinksBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'custom_help_links': result.customHelpLinks.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(HelpLink)])) - as BuiltList); + BuiltList, const [const FullType(HelpLink)]))! + as BuiltList); break; case 'default_help_links': result.defaultHelpLinks.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(HelpLink)])) - as BuiltList); + BuiltList, const [const FullType(HelpLink)]))! + as BuiltList); break; } } @@ -68,16 +67,15 @@ class _$HelpLinks extends HelpLinks { @override final BuiltList defaultHelpLinks; - factory _$HelpLinks([void Function(HelpLinksBuilder) updates]) => - (new HelpLinksBuilder()..update(updates)).build(); + factory _$HelpLinks([void Function(HelpLinksBuilder)? updates]) => + (new HelpLinksBuilder()..update(updates))._build(); - _$HelpLinks._({this.customHelpLinks, this.defaultHelpLinks}) : super._() { - if (customHelpLinks == null) { - throw new BuiltValueNullFieldError('HelpLinks', 'customHelpLinks'); - } - if (defaultHelpLinks == null) { - throw new BuiltValueNullFieldError('HelpLinks', 'defaultHelpLinks'); - } + _$HelpLinks._({required this.customHelpLinks, required this.defaultHelpLinks}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + customHelpLinks, r'HelpLinks', 'customHelpLinks'); + BuiltValueNullFieldError.checkNotNull( + defaultHelpLinks, r'HelpLinks', 'defaultHelpLinks'); } @override @@ -97,13 +95,16 @@ class _$HelpLinks extends HelpLinks { @override int get hashCode { - return $jf( - $jc($jc(0, customHelpLinks.hashCode), defaultHelpLinks.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, customHelpLinks.hashCode); + _$hash = $jc(_$hash, defaultHelpLinks.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('HelpLinks') + return (newBuiltValueToStringHelper(r'HelpLinks') ..add('customHelpLinks', customHelpLinks) ..add('defaultHelpLinks', defaultHelpLinks)) .toString(); @@ -111,26 +112,27 @@ class _$HelpLinks extends HelpLinks { } class HelpLinksBuilder implements Builder { - _$HelpLinks _$v; + _$HelpLinks? _$v; - ListBuilder _customHelpLinks; + ListBuilder? _customHelpLinks; ListBuilder get customHelpLinks => _$this._customHelpLinks ??= new ListBuilder(); - set customHelpLinks(ListBuilder customHelpLinks) => + set customHelpLinks(ListBuilder? customHelpLinks) => _$this._customHelpLinks = customHelpLinks; - ListBuilder _defaultHelpLinks; + ListBuilder? _defaultHelpLinks; ListBuilder get defaultHelpLinks => _$this._defaultHelpLinks ??= new ListBuilder(); - set defaultHelpLinks(ListBuilder defaultHelpLinks) => + set defaultHelpLinks(ListBuilder? defaultHelpLinks) => _$this._defaultHelpLinks = defaultHelpLinks; HelpLinksBuilder(); HelpLinksBuilder get _$this { - if (_$v != null) { - _customHelpLinks = _$v.customHelpLinks?.toBuilder(); - _defaultHelpLinks = _$v.defaultHelpLinks?.toBuilder(); + final $v = _$v; + if ($v != null) { + _customHelpLinks = $v.customHelpLinks.toBuilder(); + _defaultHelpLinks = $v.defaultHelpLinks.toBuilder(); _$v = null; } return this; @@ -138,19 +140,19 @@ class HelpLinksBuilder implements Builder { @override void replace(HelpLinks other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$HelpLinks; } @override - void update(void Function(HelpLinksBuilder) updates) { + void update(void Function(HelpLinksBuilder)? updates) { if (updates != null) updates(this); } @override - _$HelpLinks build() { + HelpLinks build() => _build(); + + _$HelpLinks _build() { _$HelpLinks _$result; try { _$result = _$v ?? @@ -158,7 +160,7 @@ class HelpLinksBuilder implements Builder { customHelpLinks: customHelpLinks.build(), defaultHelpLinks: defaultHelpLinks.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'customHelpLinks'; customHelpLinks.build(); @@ -166,7 +168,7 @@ class HelpLinksBuilder implements Builder { defaultHelpLinks.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'HelpLinks', _$failedField, e.toString()); + r'HelpLinks', _$failedField, e.toString()); } rethrow; } @@ -175,4 +177,4 @@ class HelpLinksBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/lock_info.dart b/apps/flutter_parent/lib/models/lock_info.dart index 5ea909aef3..e62a7c6302 100644 --- a/apps/flutter_parent/lib/models/lock_info.dart +++ b/apps/flutter_parent/lib/models/lock_info.dart @@ -27,27 +27,24 @@ abstract class LockInfo implements Built { factory LockInfo([void Function(LockInfoBuilder) updates]) = _$LockInfo; - @nullable @BuiltValueField(wireName: 'context_module') - LockedModule get contextModule; + LockedModule? get contextModule; - @nullable @BuiltValueField(wireName: 'unlock_at') - DateTime get unlockAt; + DateTime? get unlockAt; - @nullable @BuiltValueField(serialize: false) - List get modulePrerequisiteNames; + List? get modulePrerequisiteNames; @BuiltValueField(serialize: false) bool get isEmpty { return (contextModule?.name == null && - (modulePrerequisiteNames == null || modulePrerequisiteNames.length == 0) && + (modulePrerequisiteNames == null || modulePrerequisiteNames?.length == 0) && unlockAt == null); } @BuiltValueField(serialize: false) bool get hasModuleName { - return contextModule?.name != null && contextModule.name.isNotEmpty && contextModule.name != 'null'; + return contextModule?.name != null && contextModule?.name?.isNotEmpty == true && contextModule?.name != 'null'; } } diff --git a/apps/flutter_parent/lib/models/lock_info.g.dart b/apps/flutter_parent/lib/models/lock_info.g.dart index 6c33fc4d00..286ed068aa 100644 --- a/apps/flutter_parent/lib/models/lock_info.g.dart +++ b/apps/flutter_parent/lib/models/lock_info.g.dart @@ -15,45 +15,44 @@ class _$LockInfoSerializer implements StructuredSerializer { final String wireName = 'LockInfo'; @override - Iterable serialize(Serializers serializers, LockInfo object, + Iterable serialize(Serializers serializers, LockInfo object, {FullType specifiedType = FullType.unspecified}) { - final result = []; - result.add('context_module'); - if (object.contextModule == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contextModule, + final result = []; + Object? value; + value = object.contextModule; + + result + ..add('context_module') + ..add(serializers.serialize(value, specifiedType: const FullType(LockedModule))); - } - result.add('unlock_at'); - if (object.unlockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.unlockAt, + value = object.unlockAt; + + result + ..add('unlock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override - LockInfo deserialize(Serializers serializers, Iterable serialized, + LockInfo deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new LockInfoBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'context_module': result.contextModule.replace(serializers.deserialize(value, - specifiedType: const FullType(LockedModule)) as LockedModule); + specifiedType: const FullType(LockedModule))! as LockedModule); break; case 'unlock_at': result.unlockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; } } @@ -64,14 +63,14 @@ class _$LockInfoSerializer implements StructuredSerializer { class _$LockInfo extends LockInfo { @override - final LockedModule contextModule; + final LockedModule? contextModule; @override - final DateTime unlockAt; + final DateTime? unlockAt; @override - final List modulePrerequisiteNames; + final List? modulePrerequisiteNames; - factory _$LockInfo([void Function(LockInfoBuilder) updates]) => - (new LockInfoBuilder()..update(updates)).build(); + factory _$LockInfo([void Function(LockInfoBuilder)? updates]) => + (new LockInfoBuilder()..update(updates))._build(); _$LockInfo._( {this.contextModule, this.unlockAt, this.modulePrerequisiteNames}) @@ -95,13 +94,17 @@ class _$LockInfo extends LockInfo { @override int get hashCode { - return $jf($jc($jc($jc(0, contextModule.hashCode), unlockAt.hashCode), - modulePrerequisiteNames.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, contextModule.hashCode); + _$hash = $jc(_$hash, unlockAt.hashCode); + _$hash = $jc(_$hash, modulePrerequisiteNames.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('LockInfo') + return (newBuiltValueToStringHelper(r'LockInfo') ..add('contextModule', contextModule) ..add('unlockAt', unlockAt) ..add('modulePrerequisiteNames', modulePrerequisiteNames)) @@ -110,30 +113,31 @@ class _$LockInfo extends LockInfo { } class LockInfoBuilder implements Builder { - _$LockInfo _$v; + _$LockInfo? _$v; - LockedModuleBuilder _contextModule; + LockedModuleBuilder? _contextModule; LockedModuleBuilder get contextModule => _$this._contextModule ??= new LockedModuleBuilder(); - set contextModule(LockedModuleBuilder contextModule) => + set contextModule(LockedModuleBuilder? contextModule) => _$this._contextModule = contextModule; - DateTime _unlockAt; - DateTime get unlockAt => _$this._unlockAt; - set unlockAt(DateTime unlockAt) => _$this._unlockAt = unlockAt; + DateTime? _unlockAt; + DateTime? get unlockAt => _$this._unlockAt; + set unlockAt(DateTime? unlockAt) => _$this._unlockAt = unlockAt; - List _modulePrerequisiteNames; - List get modulePrerequisiteNames => _$this._modulePrerequisiteNames; - set modulePrerequisiteNames(List modulePrerequisiteNames) => + List? _modulePrerequisiteNames; + List? get modulePrerequisiteNames => _$this._modulePrerequisiteNames; + set modulePrerequisiteNames(List? modulePrerequisiteNames) => _$this._modulePrerequisiteNames = modulePrerequisiteNames; LockInfoBuilder(); LockInfoBuilder get _$this { - if (_$v != null) { - _contextModule = _$v.contextModule?.toBuilder(); - _unlockAt = _$v.unlockAt; - _modulePrerequisiteNames = _$v.modulePrerequisiteNames; + final $v = _$v; + if ($v != null) { + _contextModule = $v.contextModule?.toBuilder(); + _unlockAt = $v.unlockAt; + _modulePrerequisiteNames = $v.modulePrerequisiteNames; _$v = null; } return this; @@ -141,19 +145,19 @@ class LockInfoBuilder implements Builder { @override void replace(LockInfo other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$LockInfo; } @override - void update(void Function(LockInfoBuilder) updates) { + void update(void Function(LockInfoBuilder)? updates) { if (updates != null) updates(this); } @override - _$LockInfo build() { + LockInfo build() => _build(); + + _$LockInfo _build() { _$LockInfo _$result; try { _$result = _$v ?? @@ -162,13 +166,13 @@ class LockInfoBuilder implements Builder { unlockAt: unlockAt, modulePrerequisiteNames: modulePrerequisiteNames); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'contextModule'; _contextModule?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'LockInfo', _$failedField, e.toString()); + r'LockInfo', _$failedField, e.toString()); } rethrow; } @@ -177,4 +181,4 @@ class LockInfoBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/locked_module.dart b/apps/flutter_parent/lib/models/locked_module.dart index 840a19c69b..dc9d183ae8 100644 --- a/apps/flutter_parent/lib/models/locked_module.dart +++ b/apps/flutter_parent/lib/models/locked_module.dart @@ -31,24 +31,14 @@ abstract class LockedModule implements Built @BuiltValueField(wireName: 'context_id') String get contextId; - @nullable @BuiltValueField(wireName: 'context_type') - String get contextType; + String? get contextType; - @nullable - String get name; + String? get name; - @nullable @BuiltValueField(wireName: 'unlock_at') - DateTime get unlockAt; + DateTime? get unlockAt; - @nullable @BuiltValueField(wireName: 'require_sequential_progress') - bool get isRequireSequentialProgress; - -// val prerequisites: List? = arrayListOf(), - -// @BuiltValueField(wireName: 'completion_requirements') -// List get completionRequirements; - + bool? get isRequireSequentialProgress; } diff --git a/apps/flutter_parent/lib/models/locked_module.g.dart b/apps/flutter_parent/lib/models/locked_module.g.dart index 3ba426d9f2..dd8d5ea577 100644 --- a/apps/flutter_parent/lib/models/locked_module.g.dart +++ b/apps/flutter_parent/lib/models/locked_module.g.dart @@ -16,81 +16,78 @@ class _$LockedModuleSerializer implements StructuredSerializer { final String wireName = 'LockedModule'; @override - Iterable serialize(Serializers serializers, LockedModule object, + Iterable serialize(Serializers serializers, LockedModule object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'context_id', serializers.serialize(object.contextId, specifiedType: const FullType(String)), ]; - result.add('context_type'); - if (object.contextType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contextType, - specifiedType: const FullType(String))); - } - result.add('name'); - if (object.name == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.name, - specifiedType: const FullType(String))); - } - result.add('unlock_at'); - if (object.unlockAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.unlockAt, + Object? value; + value = object.contextType; + + result + ..add('context_type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.name; + + result + ..add('name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.unlockAt; + + result + ..add('unlock_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('require_sequential_progress'); - if (object.isRequireSequentialProgress == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.isRequireSequentialProgress, - specifiedType: const FullType(bool))); - } + value = object.isRequireSequentialProgress; + + result + ..add('require_sequential_progress') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + return result; } @override - LockedModule deserialize(Serializers serializers, Iterable serialized, + LockedModule deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new LockedModuleBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'context_id': result.contextId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'context_type': result.contextType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'unlock_at': result.unlockAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'require_sequential_progress': result.isRequireSequentialProgress = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; } } @@ -105,31 +102,28 @@ class _$LockedModule extends LockedModule { @override final String contextId; @override - final String contextType; + final String? contextType; @override - final String name; + final String? name; @override - final DateTime unlockAt; + final DateTime? unlockAt; @override - final bool isRequireSequentialProgress; + final bool? isRequireSequentialProgress; - factory _$LockedModule([void Function(LockedModuleBuilder) updates]) => - (new LockedModuleBuilder()..update(updates)).build(); + factory _$LockedModule([void Function(LockedModuleBuilder)? updates]) => + (new LockedModuleBuilder()..update(updates))._build(); _$LockedModule._( - {this.id, - this.contextId, + {required this.id, + required this.contextId, this.contextType, this.name, this.unlockAt, this.isRequireSequentialProgress}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('LockedModule', 'id'); - } - if (contextId == null) { - throw new BuiltValueNullFieldError('LockedModule', 'contextId'); - } + BuiltValueNullFieldError.checkNotNull(id, r'LockedModule', 'id'); + BuiltValueNullFieldError.checkNotNull( + contextId, r'LockedModule', 'contextId'); } @override @@ -153,19 +147,20 @@ class _$LockedModule extends LockedModule { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), contextId.hashCode), - contextType.hashCode), - name.hashCode), - unlockAt.hashCode), - isRequireSequentialProgress.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, contextId.hashCode); + _$hash = $jc(_$hash, contextType.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, unlockAt.hashCode); + _$hash = $jc(_$hash, isRequireSequentialProgress.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('LockedModule') + return (newBuiltValueToStringHelper(r'LockedModule') ..add('id', id) ..add('contextId', contextId) ..add('contextType', contextType) @@ -178,43 +173,44 @@ class _$LockedModule extends LockedModule { class LockedModuleBuilder implements Builder { - _$LockedModule _$v; + _$LockedModule? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _contextId; - String get contextId => _$this._contextId; - set contextId(String contextId) => _$this._contextId = contextId; + String? _contextId; + String? get contextId => _$this._contextId; + set contextId(String? contextId) => _$this._contextId = contextId; - String _contextType; - String get contextType => _$this._contextType; - set contextType(String contextType) => _$this._contextType = contextType; + String? _contextType; + String? get contextType => _$this._contextType; + set contextType(String? contextType) => _$this._contextType = contextType; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - DateTime _unlockAt; - DateTime get unlockAt => _$this._unlockAt; - set unlockAt(DateTime unlockAt) => _$this._unlockAt = unlockAt; + DateTime? _unlockAt; + DateTime? get unlockAt => _$this._unlockAt; + set unlockAt(DateTime? unlockAt) => _$this._unlockAt = unlockAt; - bool _isRequireSequentialProgress; - bool get isRequireSequentialProgress => _$this._isRequireSequentialProgress; - set isRequireSequentialProgress(bool isRequireSequentialProgress) => + bool? _isRequireSequentialProgress; + bool? get isRequireSequentialProgress => _$this._isRequireSequentialProgress; + set isRequireSequentialProgress(bool? isRequireSequentialProgress) => _$this._isRequireSequentialProgress = isRequireSequentialProgress; LockedModuleBuilder(); LockedModuleBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _contextId = _$v.contextId; - _contextType = _$v.contextType; - _name = _$v.name; - _unlockAt = _$v.unlockAt; - _isRequireSequentialProgress = _$v.isRequireSequentialProgress; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _contextId = $v.contextId; + _contextType = $v.contextType; + _name = $v.name; + _unlockAt = $v.unlockAt; + _isRequireSequentialProgress = $v.isRequireSequentialProgress; _$v = null; } return this; @@ -222,23 +218,25 @@ class LockedModuleBuilder @override void replace(LockedModule other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$LockedModule; } @override - void update(void Function(LockedModuleBuilder) updates) { + void update(void Function(LockedModuleBuilder)? updates) { if (updates != null) updates(this); } @override - _$LockedModule build() { + LockedModule build() => _build(); + + _$LockedModule _build() { final _$result = _$v ?? new _$LockedModule._( - id: id, - contextId: contextId, + id: BuiltValueNullFieldError.checkNotNull( + id, r'LockedModule', 'id'), + contextId: BuiltValueNullFieldError.checkNotNull( + contextId, r'LockedModule', 'contextId'), contextType: contextType, name: name, unlockAt: unlockAt, @@ -248,4 +246,4 @@ class LockedModuleBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/login.dart b/apps/flutter_parent/lib/models/login.dart index 0ae857a810..c0615fe0ac 100644 --- a/apps/flutter_parent/lib/models/login.dart +++ b/apps/flutter_parent/lib/models/login.dart @@ -29,11 +29,9 @@ abstract class Login implements Built { String get domain; - @nullable - String get clientId; + String? get clientId; - @nullable - String get clientSecret; + String? get clientSecret; String get accessToken; @@ -41,20 +39,15 @@ abstract class Login implements Built { User get user; - @nullable - String get selectedStudentId; + String? get selectedStudentId; - @nullable - bool get canMasquerade; + bool? get canMasquerade; - @nullable - User get masqueradeUser; + User? get masqueradeUser; - @nullable - String get masqueradeDomain; + String? get masqueradeDomain; - @nullable - bool get isMasqueradingFromQRCode; + bool? get isMasqueradingFromQRCode; bool get isMasquerading => masqueradeUser != null && masqueradeDomain != null; diff --git a/apps/flutter_parent/lib/models/login.g.dart b/apps/flutter_parent/lib/models/login.g.dart index 530078d50a..83373d0b54 100644 --- a/apps/flutter_parent/lib/models/login.g.dart +++ b/apps/flutter_parent/lib/models/login.g.dart @@ -15,9 +15,9 @@ class _$LoginSerializer implements StructuredSerializer { final String wireName = 'Login'; @override - Iterable serialize(Serializers serializers, Login object, + Iterable serialize(Serializers serializers, Login object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'uuid', serializers.serialize(object.uuid, specifiedType: const FullType(String)), 'domain', @@ -32,117 +32,108 @@ class _$LoginSerializer implements StructuredSerializer { 'user', serializers.serialize(object.user, specifiedType: const FullType(User)), ]; - result.add('clientId'); - if (object.clientId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.clientId, - specifiedType: const FullType(String))); - } - result.add('clientSecret'); - if (object.clientSecret == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.clientSecret, - specifiedType: const FullType(String))); - } - result.add('selectedStudentId'); - if (object.selectedStudentId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.selectedStudentId, - specifiedType: const FullType(String))); - } - result.add('canMasquerade'); - if (object.canMasquerade == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.canMasquerade, - specifiedType: const FullType(bool))); - } - result.add('masqueradeUser'); - if (object.masqueradeUser == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.masqueradeUser, - specifiedType: const FullType(User))); - } - result.add('masqueradeDomain'); - if (object.masqueradeDomain == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.masqueradeDomain, - specifiedType: const FullType(String))); - } - result.add('isMasqueradingFromQRCode'); - if (object.isMasqueradingFromQRCode == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.isMasqueradingFromQRCode, - specifiedType: const FullType(bool))); - } + Object? value; + value = object.clientId; + + result + ..add('clientId') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.clientSecret; + + result + ..add('clientSecret') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.selectedStudentId; + + result + ..add('selectedStudentId') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.canMasquerade; + + result + ..add('canMasquerade') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.masqueradeUser; + + result + ..add('masqueradeUser') + ..add(serializers.serialize(value, specifiedType: const FullType(User))); + value = object.masqueradeDomain; + + result + ..add('masqueradeDomain') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.isMasqueradingFromQRCode; + + result + ..add('isMasqueradingFromQRCode') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + return result; } @override - Login deserialize(Serializers serializers, Iterable serialized, + Login deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new LoginBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'uuid': result.uuid = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'domain': result.domain = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'clientId': result.clientId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'clientSecret': result.clientSecret = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'accessToken': result.accessToken = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'refreshToken': result.refreshToken = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'user': result.user.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; case 'selectedStudentId': result.selectedStudentId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'canMasquerade': result.canMasquerade = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'masqueradeUser': result.masqueradeUser.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; case 'masqueradeDomain': result.masqueradeDomain = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'isMasqueradingFromQRCode': result.isMasqueradingFromQRCode = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; } } @@ -157,9 +148,9 @@ class _$Login extends Login { @override final String domain; @override - final String clientId; + final String? clientId; @override - final String clientSecret; + final String? clientSecret; @override final String accessToken; @override @@ -167,48 +158,39 @@ class _$Login extends Login { @override final User user; @override - final String selectedStudentId; + final String? selectedStudentId; @override - final bool canMasquerade; + final bool? canMasquerade; @override - final User masqueradeUser; + final User? masqueradeUser; @override - final String masqueradeDomain; + final String? masqueradeDomain; @override - final bool isMasqueradingFromQRCode; + final bool? isMasqueradingFromQRCode; - factory _$Login([void Function(LoginBuilder) updates]) => - (new LoginBuilder()..update(updates)).build(); + factory _$Login([void Function(LoginBuilder)? updates]) => + (new LoginBuilder()..update(updates))._build(); _$Login._( - {this.uuid, - this.domain, + {required this.uuid, + required this.domain, this.clientId, this.clientSecret, - this.accessToken, - this.refreshToken, - this.user, + required this.accessToken, + required this.refreshToken, + required this.user, this.selectedStudentId, this.canMasquerade, this.masqueradeUser, this.masqueradeDomain, this.isMasqueradingFromQRCode}) : super._() { - if (uuid == null) { - throw new BuiltValueNullFieldError('Login', 'uuid'); - } - if (domain == null) { - throw new BuiltValueNullFieldError('Login', 'domain'); - } - if (accessToken == null) { - throw new BuiltValueNullFieldError('Login', 'accessToken'); - } - if (refreshToken == null) { - throw new BuiltValueNullFieldError('Login', 'refreshToken'); - } - if (user == null) { - throw new BuiltValueNullFieldError('Login', 'user'); - } + BuiltValueNullFieldError.checkNotNull(uuid, r'Login', 'uuid'); + BuiltValueNullFieldError.checkNotNull(domain, r'Login', 'domain'); + BuiltValueNullFieldError.checkNotNull(accessToken, r'Login', 'accessToken'); + BuiltValueNullFieldError.checkNotNull( + refreshToken, r'Login', 'refreshToken'); + BuiltValueNullFieldError.checkNotNull(user, r'Login', 'user'); } @override @@ -238,33 +220,26 @@ class _$Login extends Login { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc(0, uuid.hashCode), - domain.hashCode), - clientId.hashCode), - clientSecret.hashCode), - accessToken.hashCode), - refreshToken.hashCode), - user.hashCode), - selectedStudentId.hashCode), - canMasquerade.hashCode), - masqueradeUser.hashCode), - masqueradeDomain.hashCode), - isMasqueradingFromQRCode.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, uuid.hashCode); + _$hash = $jc(_$hash, domain.hashCode); + _$hash = $jc(_$hash, clientId.hashCode); + _$hash = $jc(_$hash, clientSecret.hashCode); + _$hash = $jc(_$hash, accessToken.hashCode); + _$hash = $jc(_$hash, refreshToken.hashCode); + _$hash = $jc(_$hash, user.hashCode); + _$hash = $jc(_$hash, selectedStudentId.hashCode); + _$hash = $jc(_$hash, canMasquerade.hashCode); + _$hash = $jc(_$hash, masqueradeUser.hashCode); + _$hash = $jc(_$hash, masqueradeDomain.hashCode); + _$hash = $jc(_$hash, isMasqueradingFromQRCode.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Login') + return (newBuiltValueToStringHelper(r'Login') ..add('uuid', uuid) ..add('domain', domain) ..add('clientId', clientId) @@ -282,60 +257,60 @@ class _$Login extends Login { } class LoginBuilder implements Builder { - _$Login _$v; + _$Login? _$v; - String _uuid; - String get uuid => _$this._uuid; - set uuid(String uuid) => _$this._uuid = uuid; + String? _uuid; + String? get uuid => _$this._uuid; + set uuid(String? uuid) => _$this._uuid = uuid; - String _domain; - String get domain => _$this._domain; - set domain(String domain) => _$this._domain = domain; + String? _domain; + String? get domain => _$this._domain; + set domain(String? domain) => _$this._domain = domain; - String _clientId; - String get clientId => _$this._clientId; - set clientId(String clientId) => _$this._clientId = clientId; + String? _clientId; + String? get clientId => _$this._clientId; + set clientId(String? clientId) => _$this._clientId = clientId; - String _clientSecret; - String get clientSecret => _$this._clientSecret; - set clientSecret(String clientSecret) => _$this._clientSecret = clientSecret; + String? _clientSecret; + String? get clientSecret => _$this._clientSecret; + set clientSecret(String? clientSecret) => _$this._clientSecret = clientSecret; - String _accessToken; - String get accessToken => _$this._accessToken; - set accessToken(String accessToken) => _$this._accessToken = accessToken; + String? _accessToken; + String? get accessToken => _$this._accessToken; + set accessToken(String? accessToken) => _$this._accessToken = accessToken; - String _refreshToken; - String get refreshToken => _$this._refreshToken; - set refreshToken(String refreshToken) => _$this._refreshToken = refreshToken; + String? _refreshToken; + String? get refreshToken => _$this._refreshToken; + set refreshToken(String? refreshToken) => _$this._refreshToken = refreshToken; - UserBuilder _user; + UserBuilder? _user; UserBuilder get user => _$this._user ??= new UserBuilder(); - set user(UserBuilder user) => _$this._user = user; + set user(UserBuilder? user) => _$this._user = user; - String _selectedStudentId; - String get selectedStudentId => _$this._selectedStudentId; - set selectedStudentId(String selectedStudentId) => + String? _selectedStudentId; + String? get selectedStudentId => _$this._selectedStudentId; + set selectedStudentId(String? selectedStudentId) => _$this._selectedStudentId = selectedStudentId; - bool _canMasquerade; - bool get canMasquerade => _$this._canMasquerade; - set canMasquerade(bool canMasquerade) => + bool? _canMasquerade; + bool? get canMasquerade => _$this._canMasquerade; + set canMasquerade(bool? canMasquerade) => _$this._canMasquerade = canMasquerade; - UserBuilder _masqueradeUser; + UserBuilder? _masqueradeUser; UserBuilder get masqueradeUser => _$this._masqueradeUser ??= new UserBuilder(); - set masqueradeUser(UserBuilder masqueradeUser) => + set masqueradeUser(UserBuilder? masqueradeUser) => _$this._masqueradeUser = masqueradeUser; - String _masqueradeDomain; - String get masqueradeDomain => _$this._masqueradeDomain; - set masqueradeDomain(String masqueradeDomain) => + String? _masqueradeDomain; + String? get masqueradeDomain => _$this._masqueradeDomain; + set masqueradeDomain(String? masqueradeDomain) => _$this._masqueradeDomain = masqueradeDomain; - bool _isMasqueradingFromQRCode; - bool get isMasqueradingFromQRCode => _$this._isMasqueradingFromQRCode; - set isMasqueradingFromQRCode(bool isMasqueradingFromQRCode) => + bool? _isMasqueradingFromQRCode; + bool? get isMasqueradingFromQRCode => _$this._isMasqueradingFromQRCode; + set isMasqueradingFromQRCode(bool? isMasqueradingFromQRCode) => _$this._isMasqueradingFromQRCode = isMasqueradingFromQRCode; LoginBuilder() { @@ -343,19 +318,20 @@ class LoginBuilder implements Builder { } LoginBuilder get _$this { - if (_$v != null) { - _uuid = _$v.uuid; - _domain = _$v.domain; - _clientId = _$v.clientId; - _clientSecret = _$v.clientSecret; - _accessToken = _$v.accessToken; - _refreshToken = _$v.refreshToken; - _user = _$v.user?.toBuilder(); - _selectedStudentId = _$v.selectedStudentId; - _canMasquerade = _$v.canMasquerade; - _masqueradeUser = _$v.masqueradeUser?.toBuilder(); - _masqueradeDomain = _$v.masqueradeDomain; - _isMasqueradingFromQRCode = _$v.isMasqueradingFromQRCode; + final $v = _$v; + if ($v != null) { + _uuid = $v.uuid; + _domain = $v.domain; + _clientId = $v.clientId; + _clientSecret = $v.clientSecret; + _accessToken = $v.accessToken; + _refreshToken = $v.refreshToken; + _user = $v.user.toBuilder(); + _selectedStudentId = $v.selectedStudentId; + _canMasquerade = $v.canMasquerade; + _masqueradeUser = $v.masqueradeUser?.toBuilder(); + _masqueradeDomain = $v.masqueradeDomain; + _isMasqueradingFromQRCode = $v.isMasqueradingFromQRCode; _$v = null; } return this; @@ -363,29 +339,33 @@ class LoginBuilder implements Builder { @override void replace(Login other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Login; } @override - void update(void Function(LoginBuilder) updates) { + void update(void Function(LoginBuilder)? updates) { if (updates != null) updates(this); } @override - _$Login build() { + Login build() => _build(); + + _$Login _build() { _$Login _$result; try { _$result = _$v ?? new _$Login._( - uuid: uuid, - domain: domain, + uuid: + BuiltValueNullFieldError.checkNotNull(uuid, r'Login', 'uuid'), + domain: BuiltValueNullFieldError.checkNotNull( + domain, r'Login', 'domain'), clientId: clientId, clientSecret: clientSecret, - accessToken: accessToken, - refreshToken: refreshToken, + accessToken: BuiltValueNullFieldError.checkNotNull( + accessToken, r'Login', 'accessToken'), + refreshToken: BuiltValueNullFieldError.checkNotNull( + refreshToken, r'Login', 'refreshToken'), user: user.build(), selectedStudentId: selectedStudentId, canMasquerade: canMasquerade, @@ -393,7 +373,7 @@ class LoginBuilder implements Builder { masqueradeDomain: masqueradeDomain, isMasqueradingFromQRCode: isMasqueradingFromQRCode); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'user'; user.build(); @@ -402,7 +382,7 @@ class LoginBuilder implements Builder { _masqueradeUser?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Login', _$failedField, e.toString()); + r'Login', _$failedField, e.toString()); } rethrow; } @@ -411,4 +391,4 @@ class LoginBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/media_comment.dart b/apps/flutter_parent/lib/models/media_comment.dart index f3fb57b0b3..f10d9d9995 100644 --- a/apps/flutter_parent/lib/models/media_comment.dart +++ b/apps/flutter_parent/lib/models/media_comment.dart @@ -28,23 +28,19 @@ abstract class MediaComment implements Built static Serializer get serializer => _$mediaCommentSerializer; @BuiltValueField(wireName: 'media_id') - @nullable - String get mediaId; + String? get mediaId; @BuiltValueField(wireName: 'display_name') - @nullable - String get displayName; + String? get displayName; - @nullable - String get url; + String? get url; /// Can be either 'audio' or 'video' @BuiltValueField(wireName: 'media_type') MediaType get mediaType; @BuiltValueField(wireName: 'content-type') - @nullable - String get contentType; + String? get contentType; MediaComment._(); factory MediaComment([void Function(MediaCommentBuilder) updates]) = _$MediaComment; diff --git a/apps/flutter_parent/lib/models/media_comment.g.dart b/apps/flutter_parent/lib/models/media_comment.g.dart index 2890cef639..5b963aa3c4 100644 --- a/apps/flutter_parent/lib/models/media_comment.g.dart +++ b/apps/flutter_parent/lib/models/media_comment.g.dart @@ -41,75 +41,73 @@ class _$MediaCommentSerializer implements StructuredSerializer { final String wireName = 'MediaComment'; @override - Iterable serialize(Serializers serializers, MediaComment object, + Iterable serialize(Serializers serializers, MediaComment object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'media_type', serializers.serialize(object.mediaType, specifiedType: const FullType(MediaType)), ]; - result.add('media_id'); - if (object.mediaId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.mediaId, - specifiedType: const FullType(String))); - } - result.add('display_name'); - if (object.displayName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.displayName, - specifiedType: const FullType(String))); - } - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('content-type'); - if (object.contentType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contentType, - specifiedType: const FullType(String))); - } + Object? value; + value = object.mediaId; + + result + ..add('media_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.displayName; + + result + ..add('display_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.contentType; + + result + ..add('content-type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - MediaComment deserialize(Serializers serializers, Iterable serialized, + MediaComment deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new MediaCommentBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'media_id': result.mediaId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'display_name': result.displayName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'media_type': result.mediaType = serializers.deserialize(value, - specifiedType: const FullType(MediaType)) as MediaType; + specifiedType: const FullType(MediaType))! as MediaType; break; case 'content-type': result.contentType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -137,29 +135,28 @@ class _$MediaTypeSerializer implements PrimitiveSerializer { class _$MediaComment extends MediaComment { @override - final String mediaId; + final String? mediaId; @override - final String displayName; + final String? displayName; @override - final String url; + final String? url; @override final MediaType mediaType; @override - final String contentType; + final String? contentType; - factory _$MediaComment([void Function(MediaCommentBuilder) updates]) => - (new MediaCommentBuilder()..update(updates)).build(); + factory _$MediaComment([void Function(MediaCommentBuilder)? updates]) => + (new MediaCommentBuilder()..update(updates))._build(); _$MediaComment._( {this.mediaId, this.displayName, this.url, - this.mediaType, + required this.mediaType, this.contentType}) : super._() { - if (mediaType == null) { - throw new BuiltValueNullFieldError('MediaComment', 'mediaType'); - } + BuiltValueNullFieldError.checkNotNull( + mediaType, r'MediaComment', 'mediaType'); } @override @@ -182,17 +179,19 @@ class _$MediaComment extends MediaComment { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc(0, mediaId.hashCode), displayName.hashCode), - url.hashCode), - mediaType.hashCode), - contentType.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, mediaId.hashCode); + _$hash = $jc(_$hash, displayName.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, mediaType.hashCode); + _$hash = $jc(_$hash, contentType.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('MediaComment') + return (newBuiltValueToStringHelper(r'MediaComment') ..add('mediaId', mediaId) ..add('displayName', displayName) ..add('url', url) @@ -204,37 +203,38 @@ class _$MediaComment extends MediaComment { class MediaCommentBuilder implements Builder { - _$MediaComment _$v; + _$MediaComment? _$v; - String _mediaId; - String get mediaId => _$this._mediaId; - set mediaId(String mediaId) => _$this._mediaId = mediaId; + String? _mediaId; + String? get mediaId => _$this._mediaId; + set mediaId(String? mediaId) => _$this._mediaId = mediaId; - String _displayName; - String get displayName => _$this._displayName; - set displayName(String displayName) => _$this._displayName = displayName; + String? _displayName; + String? get displayName => _$this._displayName; + set displayName(String? displayName) => _$this._displayName = displayName; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - MediaType _mediaType; - MediaType get mediaType => _$this._mediaType; - set mediaType(MediaType mediaType) => _$this._mediaType = mediaType; + MediaType? _mediaType; + MediaType? get mediaType => _$this._mediaType; + set mediaType(MediaType? mediaType) => _$this._mediaType = mediaType; - String _contentType; - String get contentType => _$this._contentType; - set contentType(String contentType) => _$this._contentType = contentType; + String? _contentType; + String? get contentType => _$this._contentType; + set contentType(String? contentType) => _$this._contentType = contentType; MediaCommentBuilder(); MediaCommentBuilder get _$this { - if (_$v != null) { - _mediaId = _$v.mediaId; - _displayName = _$v.displayName; - _url = _$v.url; - _mediaType = _$v.mediaType; - _contentType = _$v.contentType; + final $v = _$v; + if ($v != null) { + _mediaId = $v.mediaId; + _displayName = $v.displayName; + _url = $v.url; + _mediaType = $v.mediaType; + _contentType = $v.contentType; _$v = null; } return this; @@ -242,29 +242,30 @@ class MediaCommentBuilder @override void replace(MediaComment other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$MediaComment; } @override - void update(void Function(MediaCommentBuilder) updates) { + void update(void Function(MediaCommentBuilder)? updates) { if (updates != null) updates(this); } @override - _$MediaComment build() { + MediaComment build() => _build(); + + _$MediaComment _build() { final _$result = _$v ?? new _$MediaComment._( mediaId: mediaId, displayName: displayName, url: url, - mediaType: mediaType, + mediaType: BuiltValueNullFieldError.checkNotNull( + mediaType, r'MediaComment', 'mediaType'), contentType: contentType); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/message.dart b/apps/flutter_parent/lib/models/message.dart index 9252807f54..963e2b3950 100644 --- a/apps/flutter_parent/lib/models/message.dart +++ b/apps/flutter_parent/lib/models/message.dart @@ -30,11 +30,9 @@ abstract class Message implements Built { String get id; @BuiltValueField(wireName: 'created_at') - @nullable - DateTime get createdAt; + DateTime? get createdAt; - @nullable - String get body; + String? get body; @BuiltValueField(wireName: 'author_id') String get authorId; @@ -42,20 +40,16 @@ abstract class Message implements Built { @BuiltValueField(wireName: 'generated') bool get isGenerated; - @nullable - BuiltList get attachments; + BuiltList? get attachments; @BuiltValueField(wireName: 'media_comment') - @nullable - MediaComment get mediaComment; + MediaComment? get mediaComment; @BuiltValueField(wireName: 'forwarded_messages') - @nullable - BuiltList get forwardedMessages; + BuiltList? get forwardedMessages; @BuiltValueField(wireName: 'participating_user_ids') - @nullable - BuiltList get participatingUserIds; + BuiltList? get participatingUserIds; Message._(); factory Message([void Function(MessageBuilder) updates]) = _$Message; diff --git a/apps/flutter_parent/lib/models/message.g.dart b/apps/flutter_parent/lib/models/message.g.dart index bdf4a00399..c8455f141a 100644 --- a/apps/flutter_parent/lib/models/message.g.dart +++ b/apps/flutter_parent/lib/models/message.g.dart @@ -15,9 +15,9 @@ class _$MessageSerializer implements StructuredSerializer { final String wireName = 'Message'; @override - Iterable serialize(Serializers serializers, Message object, + Iterable serialize(Serializers serializers, Message object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'author_id', @@ -27,107 +27,102 @@ class _$MessageSerializer implements StructuredSerializer { serializers.serialize(object.isGenerated, specifiedType: const FullType(bool)), ]; - result.add('created_at'); - if (object.createdAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.createdAt, + Object? value; + value = object.createdAt; + + result + ..add('created_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('body'); - if (object.body == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.body, - specifiedType: const FullType(String))); - } - result.add('attachments'); - if (object.attachments == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.attachments, + value = object.body; + + result + ..add('body') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.attachments; + + result + ..add('attachments') + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltList, const [const FullType(Attachment)]))); - } - result.add('media_comment'); - if (object.mediaComment == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.mediaComment, + value = object.mediaComment; + + result + ..add('media_comment') + ..add(serializers.serialize(value, specifiedType: const FullType(MediaComment))); - } - result.add('forwarded_messages'); - if (object.forwardedMessages == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.forwardedMessages, + value = object.forwardedMessages; + + result + ..add('forwarded_messages') + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltList, const [const FullType(Message)]))); - } - result.add('participating_user_ids'); - if (object.participatingUserIds == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.participatingUserIds, + value = object.participatingUserIds; + + result + ..add('participating_user_ids') + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltList, const [const FullType(String)]))); - } + return result; } @override - Message deserialize(Serializers serializers, Iterable serialized, + Message deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new MessageBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'created_at': result.createdAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'body': result.body = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'author_id': result.authorId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'generated': result.isGenerated = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'attachments': result.attachments.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Attachment)])) - as BuiltList); + BuiltList, const [const FullType(Attachment)]))! + as BuiltList); break; case 'media_comment': result.mediaComment.replace(serializers.deserialize(value, - specifiedType: const FullType(MediaComment)) as MediaComment); + specifiedType: const FullType(MediaComment))! as MediaComment); break; case 'forwarded_messages': result.forwardedMessages.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Message)])) - as BuiltList); + BuiltList, const [const FullType(Message)]))! + as BuiltList); break; case 'participating_user_ids': result.participatingUserIds.replace(serializers.deserialize(value, - specifiedType: - const FullType(BuiltList, const [const FullType(String)])) - as BuiltList); + specifiedType: const FullType( + BuiltList, const [const FullType(String)]))! + as BuiltList); break; } } @@ -140,45 +135,40 @@ class _$Message extends Message { @override final String id; @override - final DateTime createdAt; + final DateTime? createdAt; @override - final String body; + final String? body; @override final String authorId; @override final bool isGenerated; @override - final BuiltList attachments; + final BuiltList? attachments; @override - final MediaComment mediaComment; + final MediaComment? mediaComment; @override - final BuiltList forwardedMessages; + final BuiltList? forwardedMessages; @override - final BuiltList participatingUserIds; + final BuiltList? participatingUserIds; - factory _$Message([void Function(MessageBuilder) updates]) => - (new MessageBuilder()..update(updates)).build(); + factory _$Message([void Function(MessageBuilder)? updates]) => + (new MessageBuilder()..update(updates))._build(); _$Message._( - {this.id, + {required this.id, this.createdAt, this.body, - this.authorId, - this.isGenerated, + required this.authorId, + required this.isGenerated, this.attachments, this.mediaComment, this.forwardedMessages, this.participatingUserIds}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Message', 'id'); - } - if (authorId == null) { - throw new BuiltValueNullFieldError('Message', 'authorId'); - } - if (isGenerated == null) { - throw new BuiltValueNullFieldError('Message', 'isGenerated'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Message', 'id'); + BuiltValueNullFieldError.checkNotNull(authorId, r'Message', 'authorId'); + BuiltValueNullFieldError.checkNotNull( + isGenerated, r'Message', 'isGenerated'); } @override @@ -205,25 +195,23 @@ class _$Message extends Message { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), createdAt.hashCode), - body.hashCode), - authorId.hashCode), - isGenerated.hashCode), - attachments.hashCode), - mediaComment.hashCode), - forwardedMessages.hashCode), - participatingUserIds.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, createdAt.hashCode); + _$hash = $jc(_$hash, body.hashCode); + _$hash = $jc(_$hash, authorId.hashCode); + _$hash = $jc(_$hash, isGenerated.hashCode); + _$hash = $jc(_$hash, attachments.hashCode); + _$hash = $jc(_$hash, mediaComment.hashCode); + _$hash = $jc(_$hash, forwardedMessages.hashCode); + _$hash = $jc(_$hash, participatingUserIds.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Message') + return (newBuiltValueToStringHelper(r'Message') ..add('id', id) ..add('createdAt', createdAt) ..add('body', body) @@ -238,50 +226,50 @@ class _$Message extends Message { } class MessageBuilder implements Builder { - _$Message _$v; + _$Message? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - DateTime _createdAt; - DateTime get createdAt => _$this._createdAt; - set createdAt(DateTime createdAt) => _$this._createdAt = createdAt; + DateTime? _createdAt; + DateTime? get createdAt => _$this._createdAt; + set createdAt(DateTime? createdAt) => _$this._createdAt = createdAt; - String _body; - String get body => _$this._body; - set body(String body) => _$this._body = body; + String? _body; + String? get body => _$this._body; + set body(String? body) => _$this._body = body; - String _authorId; - String get authorId => _$this._authorId; - set authorId(String authorId) => _$this._authorId = authorId; + String? _authorId; + String? get authorId => _$this._authorId; + set authorId(String? authorId) => _$this._authorId = authorId; - bool _isGenerated; - bool get isGenerated => _$this._isGenerated; - set isGenerated(bool isGenerated) => _$this._isGenerated = isGenerated; + bool? _isGenerated; + bool? get isGenerated => _$this._isGenerated; + set isGenerated(bool? isGenerated) => _$this._isGenerated = isGenerated; - ListBuilder _attachments; + ListBuilder? _attachments; ListBuilder get attachments => _$this._attachments ??= new ListBuilder(); - set attachments(ListBuilder attachments) => + set attachments(ListBuilder? attachments) => _$this._attachments = attachments; - MediaCommentBuilder _mediaComment; + MediaCommentBuilder? _mediaComment; MediaCommentBuilder get mediaComment => _$this._mediaComment ??= new MediaCommentBuilder(); - set mediaComment(MediaCommentBuilder mediaComment) => + set mediaComment(MediaCommentBuilder? mediaComment) => _$this._mediaComment = mediaComment; - ListBuilder _forwardedMessages; + ListBuilder? _forwardedMessages; ListBuilder get forwardedMessages => _$this._forwardedMessages ??= new ListBuilder(); - set forwardedMessages(ListBuilder forwardedMessages) => + set forwardedMessages(ListBuilder? forwardedMessages) => _$this._forwardedMessages = forwardedMessages; - ListBuilder _participatingUserIds; + ListBuilder? _participatingUserIds; ListBuilder get participatingUserIds => _$this._participatingUserIds ??= new ListBuilder(); - set participatingUserIds(ListBuilder participatingUserIds) => + set participatingUserIds(ListBuilder? participatingUserIds) => _$this._participatingUserIds = participatingUserIds; MessageBuilder() { @@ -289,16 +277,17 @@ class MessageBuilder implements Builder { } MessageBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _createdAt = _$v.createdAt; - _body = _$v.body; - _authorId = _$v.authorId; - _isGenerated = _$v.isGenerated; - _attachments = _$v.attachments?.toBuilder(); - _mediaComment = _$v.mediaComment?.toBuilder(); - _forwardedMessages = _$v.forwardedMessages?.toBuilder(); - _participatingUserIds = _$v.participatingUserIds?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _createdAt = $v.createdAt; + _body = $v.body; + _authorId = $v.authorId; + _isGenerated = $v.isGenerated; + _attachments = $v.attachments?.toBuilder(); + _mediaComment = $v.mediaComment?.toBuilder(); + _forwardedMessages = $v.forwardedMessages?.toBuilder(); + _participatingUserIds = $v.participatingUserIds?.toBuilder(); _$v = null; } return this; @@ -306,34 +295,36 @@ class MessageBuilder implements Builder { @override void replace(Message other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Message; } @override - void update(void Function(MessageBuilder) updates) { + void update(void Function(MessageBuilder)? updates) { if (updates != null) updates(this); } @override - _$Message build() { + Message build() => _build(); + + _$Message _build() { _$Message _$result; try { _$result = _$v ?? new _$Message._( - id: id, + id: BuiltValueNullFieldError.checkNotNull(id, r'Message', 'id'), createdAt: createdAt, body: body, - authorId: authorId, - isGenerated: isGenerated, + authorId: BuiltValueNullFieldError.checkNotNull( + authorId, r'Message', 'authorId'), + isGenerated: BuiltValueNullFieldError.checkNotNull( + isGenerated, r'Message', 'isGenerated'), attachments: _attachments?.build(), mediaComment: _mediaComment?.build(), forwardedMessages: _forwardedMessages?.build(), participatingUserIds: _participatingUserIds?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'attachments'; _attachments?.build(); @@ -345,7 +336,7 @@ class MessageBuilder implements Builder { _participatingUserIds?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Message', _$failedField, e.toString()); + r'Message', _$failedField, e.toString()); } rethrow; } @@ -354,4 +345,4 @@ class MessageBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/mobile_verify_result.dart b/apps/flutter_parent/lib/models/mobile_verify_result.dart index 7ce9795ef3..e29320e37a 100644 --- a/apps/flutter_parent/lib/models/mobile_verify_result.dart +++ b/apps/flutter_parent/lib/models/mobile_verify_result.dart @@ -76,6 +76,6 @@ class ResultEnumSerializer extends PrimitiveSerializer { @override VerifyResultEnum deserialize(Serializers serializers, Object serialized, {FullType specifiedType = FullType.unspecified}) { - return VerifyResultEnum.values[serialized]; + return VerifyResultEnum.values[serialized as int]; } } diff --git a/apps/flutter_parent/lib/models/mobile_verify_result.g.dart b/apps/flutter_parent/lib/models/mobile_verify_result.g.dart index f3cbf77e68..f97c0088fa 100644 --- a/apps/flutter_parent/lib/models/mobile_verify_result.g.dart +++ b/apps/flutter_parent/lib/models/mobile_verify_result.g.dart @@ -17,9 +17,10 @@ class _$MobileVerifyResultSerializer final String wireName = 'MobileVerifyResult'; @override - Iterable serialize(Serializers serializers, MobileVerifyResult object, + Iterable serialize( + Serializers serializers, MobileVerifyResult object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'authorized', serializers.serialize(object.authorized, specifiedType: const FullType(bool)), @@ -45,41 +46,40 @@ class _$MobileVerifyResultSerializer @override MobileVerifyResult deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new MobileVerifyResultBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'authorized': result.authorized = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'result': result.result = serializers.deserialize(value, - specifiedType: const FullType(VerifyResultEnum)) + specifiedType: const FullType(VerifyResultEnum))! as VerifyResultEnum; break; case 'client_id': result.clientId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'client_secret': result.clientSecret = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'api_key': result.apiKey = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'base_url': result.baseUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -103,35 +103,29 @@ class _$MobileVerifyResult extends MobileVerifyResult { final String baseUrl; factory _$MobileVerifyResult( - [void Function(MobileVerifyResultBuilder) updates]) => - (new MobileVerifyResultBuilder()..update(updates)).build(); + [void Function(MobileVerifyResultBuilder)? updates]) => + (new MobileVerifyResultBuilder()..update(updates))._build(); _$MobileVerifyResult._( - {this.authorized, - this.result, - this.clientId, - this.clientSecret, - this.apiKey, - this.baseUrl}) + {required this.authorized, + required this.result, + required this.clientId, + required this.clientSecret, + required this.apiKey, + required this.baseUrl}) : super._() { - if (authorized == null) { - throw new BuiltValueNullFieldError('MobileVerifyResult', 'authorized'); - } - if (result == null) { - throw new BuiltValueNullFieldError('MobileVerifyResult', 'result'); - } - if (clientId == null) { - throw new BuiltValueNullFieldError('MobileVerifyResult', 'clientId'); - } - if (clientSecret == null) { - throw new BuiltValueNullFieldError('MobileVerifyResult', 'clientSecret'); - } - if (apiKey == null) { - throw new BuiltValueNullFieldError('MobileVerifyResult', 'apiKey'); - } - if (baseUrl == null) { - throw new BuiltValueNullFieldError('MobileVerifyResult', 'baseUrl'); - } + BuiltValueNullFieldError.checkNotNull( + authorized, r'MobileVerifyResult', 'authorized'); + BuiltValueNullFieldError.checkNotNull( + result, r'MobileVerifyResult', 'result'); + BuiltValueNullFieldError.checkNotNull( + clientId, r'MobileVerifyResult', 'clientId'); + BuiltValueNullFieldError.checkNotNull( + clientSecret, r'MobileVerifyResult', 'clientSecret'); + BuiltValueNullFieldError.checkNotNull( + apiKey, r'MobileVerifyResult', 'apiKey'); + BuiltValueNullFieldError.checkNotNull( + baseUrl, r'MobileVerifyResult', 'baseUrl'); } @override @@ -157,19 +151,20 @@ class _$MobileVerifyResult extends MobileVerifyResult { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc($jc($jc(0, authorized.hashCode), result.hashCode), - clientId.hashCode), - clientSecret.hashCode), - apiKey.hashCode), - baseUrl.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, authorized.hashCode); + _$hash = $jc(_$hash, result.hashCode); + _$hash = $jc(_$hash, clientId.hashCode); + _$hash = $jc(_$hash, clientSecret.hashCode); + _$hash = $jc(_$hash, apiKey.hashCode); + _$hash = $jc(_$hash, baseUrl.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('MobileVerifyResult') + return (newBuiltValueToStringHelper(r'MobileVerifyResult') ..add('authorized', authorized) ..add('result', result) ..add('clientId', clientId) @@ -182,44 +177,45 @@ class _$MobileVerifyResult extends MobileVerifyResult { class MobileVerifyResultBuilder implements Builder { - _$MobileVerifyResult _$v; + _$MobileVerifyResult? _$v; - bool _authorized; - bool get authorized => _$this._authorized; - set authorized(bool authorized) => _$this._authorized = authorized; + bool? _authorized; + bool? get authorized => _$this._authorized; + set authorized(bool? authorized) => _$this._authorized = authorized; - VerifyResultEnum _result; - VerifyResultEnum get result => _$this._result; - set result(VerifyResultEnum result) => _$this._result = result; + VerifyResultEnum? _result; + VerifyResultEnum? get result => _$this._result; + set result(VerifyResultEnum? result) => _$this._result = result; - String _clientId; - String get clientId => _$this._clientId; - set clientId(String clientId) => _$this._clientId = clientId; + String? _clientId; + String? get clientId => _$this._clientId; + set clientId(String? clientId) => _$this._clientId = clientId; - String _clientSecret; - String get clientSecret => _$this._clientSecret; - set clientSecret(String clientSecret) => _$this._clientSecret = clientSecret; + String? _clientSecret; + String? get clientSecret => _$this._clientSecret; + set clientSecret(String? clientSecret) => _$this._clientSecret = clientSecret; - String _apiKey; - String get apiKey => _$this._apiKey; - set apiKey(String apiKey) => _$this._apiKey = apiKey; + String? _apiKey; + String? get apiKey => _$this._apiKey; + set apiKey(String? apiKey) => _$this._apiKey = apiKey; - String _baseUrl; - String get baseUrl => _$this._baseUrl; - set baseUrl(String baseUrl) => _$this._baseUrl = baseUrl; + String? _baseUrl; + String? get baseUrl => _$this._baseUrl; + set baseUrl(String? baseUrl) => _$this._baseUrl = baseUrl; MobileVerifyResultBuilder() { MobileVerifyResult._initializeBuilder(this); } MobileVerifyResultBuilder get _$this { - if (_$v != null) { - _authorized = _$v.authorized; - _result = _$v.result; - _clientId = _$v.clientId; - _clientSecret = _$v.clientSecret; - _apiKey = _$v.apiKey; - _baseUrl = _$v.baseUrl; + final $v = _$v; + if ($v != null) { + _authorized = $v.authorized; + _result = $v.result; + _clientId = $v.clientId; + _clientSecret = $v.clientSecret; + _apiKey = $v.apiKey; + _baseUrl = $v.baseUrl; _$v = null; } return this; @@ -227,30 +223,36 @@ class MobileVerifyResultBuilder @override void replace(MobileVerifyResult other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$MobileVerifyResult; } @override - void update(void Function(MobileVerifyResultBuilder) updates) { + void update(void Function(MobileVerifyResultBuilder)? updates) { if (updates != null) updates(this); } @override - _$MobileVerifyResult build() { + MobileVerifyResult build() => _build(); + + _$MobileVerifyResult _build() { final _$result = _$v ?? new _$MobileVerifyResult._( - authorized: authorized, - result: result, - clientId: clientId, - clientSecret: clientSecret, - apiKey: apiKey, - baseUrl: baseUrl); + authorized: BuiltValueNullFieldError.checkNotNull( + authorized, r'MobileVerifyResult', 'authorized'), + result: BuiltValueNullFieldError.checkNotNull( + result, r'MobileVerifyResult', 'result'), + clientId: BuiltValueNullFieldError.checkNotNull( + clientId, r'MobileVerifyResult', 'clientId'), + clientSecret: BuiltValueNullFieldError.checkNotNull( + clientSecret, r'MobileVerifyResult', 'clientSecret'), + apiKey: BuiltValueNullFieldError.checkNotNull( + apiKey, r'MobileVerifyResult', 'apiKey'), + baseUrl: BuiltValueNullFieldError.checkNotNull( + baseUrl, r'MobileVerifyResult', 'baseUrl')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/notification_payload.dart b/apps/flutter_parent/lib/models/notification_payload.dart index c91d31ee41..19ff3b1f98 100644 --- a/apps/flutter_parent/lib/models/notification_payload.dart +++ b/apps/flutter_parent/lib/models/notification_payload.dart @@ -26,8 +26,7 @@ abstract class NotificationPayload implements Built serialize( + Iterable serialize( Serializers serializers, NotificationPayload object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'type', serializers.serialize(object.type, specifiedType: const FullType(NotificationPayloadType)), ]; - result.add('data'); - if (object.data == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.data, - specifiedType: const FullType(String))); - } + Object? value; + value = object.data; + + result + ..add('data') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override NotificationPayload deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new NotificationPayloadBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(NotificationPayloadType)) + specifiedType: const FullType(NotificationPayloadType))! as NotificationPayloadType; break; case 'data': result.data = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -115,23 +115,21 @@ class _$NotificationPayloadTypeSerializer Serializers serializers, Object serialized, {FullType specifiedType = FullType.unspecified}) => NotificationPayloadType.valueOf( - _fromWire[serialized] ?? serialized as String); + _fromWire[serialized] ?? (serialized is String ? serialized : '')); } class _$NotificationPayload extends NotificationPayload { @override final NotificationPayloadType type; @override - final String data; + final String? data; factory _$NotificationPayload( - [void Function(NotificationPayloadBuilder) updates]) => - (new NotificationPayloadBuilder()..update(updates)).build(); + [void Function(NotificationPayloadBuilder)? updates]) => + (new NotificationPayloadBuilder()..update(updates))._build(); - _$NotificationPayload._({this.type, this.data}) : super._() { - if (type == null) { - throw new BuiltValueNullFieldError('NotificationPayload', 'type'); - } + _$NotificationPayload._({required this.type, this.data}) : super._() { + BuiltValueNullFieldError.checkNotNull(type, r'NotificationPayload', 'type'); } @override @@ -153,12 +151,16 @@ class _$NotificationPayload extends NotificationPayload { @override int get hashCode { - return $jf($jc($jc(0, type.hashCode), data.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, data.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('NotificationPayload') + return (newBuiltValueToStringHelper(r'NotificationPayload') ..add('type', type) ..add('data', data)) .toString(); @@ -167,22 +169,23 @@ class _$NotificationPayload extends NotificationPayload { class NotificationPayloadBuilder implements Builder { - _$NotificationPayload _$v; + _$NotificationPayload? _$v; - NotificationPayloadType _type; - NotificationPayloadType get type => _$this._type; - set type(NotificationPayloadType type) => _$this._type = type; + NotificationPayloadType? _type; + NotificationPayloadType? get type => _$this._type; + set type(NotificationPayloadType? type) => _$this._type = type; - String _data; - String get data => _$this._data; - set data(String data) => _$this._data = data; + String? _data; + String? get data => _$this._data; + set data(String? data) => _$this._data = data; NotificationPayloadBuilder(); NotificationPayloadBuilder get _$this { - if (_$v != null) { - _type = _$v.type; - _data = _$v.data; + final $v = _$v; + if ($v != null) { + _type = $v.type; + _data = $v.data; _$v = null; } return this; @@ -190,23 +193,27 @@ class NotificationPayloadBuilder @override void replace(NotificationPayload other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$NotificationPayload; } @override - void update(void Function(NotificationPayloadBuilder) updates) { + void update(void Function(NotificationPayloadBuilder)? updates) { if (updates != null) updates(this); } @override - _$NotificationPayload build() { - final _$result = _$v ?? new _$NotificationPayload._(type: type, data: data); + NotificationPayload build() => _build(); + + _$NotificationPayload _build() { + final _$result = _$v ?? + new _$NotificationPayload._( + type: BuiltValueNullFieldError.checkNotNull( + type, r'NotificationPayload', 'type'), + data: data); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/plannable.dart b/apps/flutter_parent/lib/models/plannable.dart index f6e2115598..93e20f6968 100644 --- a/apps/flutter_parent/lib/models/plannable.dart +++ b/apps/flutter_parent/lib/models/plannable.dart @@ -29,18 +29,15 @@ abstract class Plannable implements Built { String get title; - @nullable @BuiltValueField(wireName: 'points_possible') - double get pointsPossible; + double? get pointsPossible; - @nullable @BuiltValueField(wireName: 'due_at') - DateTime get dueAt; + DateTime? get dueAt; // Used to determine if a quiz is an assignment or not - @nullable @BuiltValueField(wireName: 'assignment_id') - String get assignmentId; + String? get assignmentId; factory Plannable([void Function(PlannableBuilder) updates]) = _$Plannable; } diff --git a/apps/flutter_parent/lib/models/plannable.g.dart b/apps/flutter_parent/lib/models/plannable.g.dart index ffe0074253..c225b48224 100644 --- a/apps/flutter_parent/lib/models/plannable.g.dart +++ b/apps/flutter_parent/lib/models/plannable.g.dart @@ -15,70 +15,68 @@ class _$PlannableSerializer implements StructuredSerializer { final String wireName = 'Plannable'; @override - Iterable serialize(Serializers serializers, Plannable object, + Iterable serialize(Serializers serializers, Plannable object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'title', serializers.serialize(object.title, specifiedType: const FullType(String)), ]; - result.add('points_possible'); - if (object.pointsPossible == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.pointsPossible, - specifiedType: const FullType(double))); - } - result.add('due_at'); - if (object.dueAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.dueAt, + Object? value; + value = object.pointsPossible; + + result + ..add('points_possible') + ..add( + serializers.serialize(value, specifiedType: const FullType(double))); + value = object.dueAt; + + result + ..add('due_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('assignment_id'); - if (object.assignmentId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.assignmentId, - specifiedType: const FullType(String))); - } + value = object.assignmentId; + + result + ..add('assignment_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - Plannable deserialize(Serializers serializers, Iterable serialized, + Plannable deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PlannableBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'points_possible': result.pointsPossible = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'due_at': result.dueAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'assignment_id': result.assignmentId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -93,24 +91,24 @@ class _$Plannable extends Plannable { @override final String title; @override - final double pointsPossible; + final double? pointsPossible; @override - final DateTime dueAt; + final DateTime? dueAt; @override - final String assignmentId; + final String? assignmentId; - factory _$Plannable([void Function(PlannableBuilder) updates]) => - (new PlannableBuilder()..update(updates)).build(); + factory _$Plannable([void Function(PlannableBuilder)? updates]) => + (new PlannableBuilder()..update(updates))._build(); _$Plannable._( - {this.id, this.title, this.pointsPossible, this.dueAt, this.assignmentId}) + {required this.id, + required this.title, + this.pointsPossible, + this.dueAt, + this.assignmentId}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Plannable', 'id'); - } - if (title == null) { - throw new BuiltValueNullFieldError('Plannable', 'title'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Plannable', 'id'); + BuiltValueNullFieldError.checkNotNull(title, r'Plannable', 'title'); } @override @@ -133,17 +131,19 @@ class _$Plannable extends Plannable { @override int get hashCode { - return $jf($jc( - $jc( - $jc($jc($jc(0, id.hashCode), title.hashCode), - pointsPossible.hashCode), - dueAt.hashCode), - assignmentId.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, pointsPossible.hashCode); + _$hash = $jc(_$hash, dueAt.hashCode); + _$hash = $jc(_$hash, assignmentId.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Plannable') + return (newBuiltValueToStringHelper(r'Plannable') ..add('id', id) ..add('title', title) ..add('pointsPossible', pointsPossible) @@ -154,38 +154,39 @@ class _$Plannable extends Plannable { } class PlannableBuilder implements Builder { - _$Plannable _$v; + _$Plannable? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - double _pointsPossible; - double get pointsPossible => _$this._pointsPossible; - set pointsPossible(double pointsPossible) => + double? _pointsPossible; + double? get pointsPossible => _$this._pointsPossible; + set pointsPossible(double? pointsPossible) => _$this._pointsPossible = pointsPossible; - DateTime _dueAt; - DateTime get dueAt => _$this._dueAt; - set dueAt(DateTime dueAt) => _$this._dueAt = dueAt; + DateTime? _dueAt; + DateTime? get dueAt => _$this._dueAt; + set dueAt(DateTime? dueAt) => _$this._dueAt = dueAt; - String _assignmentId; - String get assignmentId => _$this._assignmentId; - set assignmentId(String assignmentId) => _$this._assignmentId = assignmentId; + String? _assignmentId; + String? get assignmentId => _$this._assignmentId; + set assignmentId(String? assignmentId) => _$this._assignmentId = assignmentId; PlannableBuilder(); PlannableBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _title = _$v.title; - _pointsPossible = _$v.pointsPossible; - _dueAt = _$v.dueAt; - _assignmentId = _$v.assignmentId; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _title = $v.title; + _pointsPossible = $v.pointsPossible; + _dueAt = $v.dueAt; + _assignmentId = $v.assignmentId; _$v = null; } return this; @@ -193,23 +194,24 @@ class PlannableBuilder implements Builder { @override void replace(Plannable other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Plannable; } @override - void update(void Function(PlannableBuilder) updates) { + void update(void Function(PlannableBuilder)? updates) { if (updates != null) updates(this); } @override - _$Plannable build() { + Plannable build() => _build(); + + _$Plannable _build() { final _$result = _$v ?? new _$Plannable._( - id: id, - title: title, + id: BuiltValueNullFieldError.checkNotNull(id, r'Plannable', 'id'), + title: BuiltValueNullFieldError.checkNotNull( + title, r'Plannable', 'title'), pointsPossible: pointsPossible, dueAt: dueAt, assignmentId: assignmentId); @@ -218,4 +220,4 @@ class PlannableBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/planner_item.dart b/apps/flutter_parent/lib/models/planner_item.dart index c239515a55..ee8258791c 100644 --- a/apps/flutter_parent/lib/models/planner_item.dart +++ b/apps/flutter_parent/lib/models/planner_item.dart @@ -28,43 +28,30 @@ abstract class PlannerItem implements Built { PlannerItem._(); - @nullable @BuiltValueField(wireName: 'course_id') - String get courseId; + String? get courseId; - @nullable @BuiltValueField(wireName: 'context_type') - String get contextType; + String? get contextType; - @nullable @BuiltValueField(wireName: 'context_name') - String get contextName; + String? get contextName; @BuiltValueField(wireName: 'plannable_type') String get plannableType; Plannable get plannable; - @nullable @BuiltValueField(wireName: 'plannable_date') - DateTime get plannableDate; + DateTime? get plannableDate; - @nullable @BuiltValueField(wireName: 'submissions') - JsonObject get submissionStatusRaw; + JsonObject? get submissionStatusRaw; - @nullable @BuiltValueField(wireName: 'html_url') - String get htmlUrl; + String? get htmlUrl; - @nullable - PlannerSubmission get submissionStatus; - -// @nullable TODO - keep in place for potentially moving back to planner api -// PlannerSubmission get submissionStatus { -// if (submissionStatusRaw == null || submissionStatusRaw.isBool) return null; -// return deserialize(submissionStatusRaw.value); -// } + PlannerSubmission? get submissionStatus; factory PlannerItem([void Function(PlannerItemBuilder) updates]) = _$PlannerItem; } diff --git a/apps/flutter_parent/lib/models/planner_item.g.dart b/apps/flutter_parent/lib/models/planner_item.g.dart index 869868e315..e4b087c9b9 100644 --- a/apps/flutter_parent/lib/models/planner_item.g.dart +++ b/apps/flutter_parent/lib/models/planner_item.g.dart @@ -15,9 +15,9 @@ class _$PlannerItemSerializer implements StructuredSerializer { final String wireName = 'PlannerItem'; @override - Iterable serialize(Serializers serializers, PlannerItem object, + Iterable serialize(Serializers serializers, PlannerItem object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'plannable_type', serializers.serialize(object.plannableType, specifiedType: const FullType(String)), @@ -25,105 +25,99 @@ class _$PlannerItemSerializer implements StructuredSerializer { serializers.serialize(object.plannable, specifiedType: const FullType(Plannable)), ]; - result.add('course_id'); - if (object.courseId == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.courseId, - specifiedType: const FullType(String))); - } - result.add('context_type'); - if (object.contextType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contextType, - specifiedType: const FullType(String))); - } - result.add('context_name'); - if (object.contextName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contextName, - specifiedType: const FullType(String))); - } - result.add('plannable_date'); - if (object.plannableDate == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.plannableDate, + Object? value; + value = object.courseId; + + result + ..add('course_id') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.contextType; + + result + ..add('context_type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.contextName; + + result + ..add('context_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.plannableDate; + + result + ..add('plannable_date') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('submissions'); - if (object.submissionStatusRaw == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionStatusRaw, + value = object.submissionStatusRaw; + + result + ..add('submissions') + ..add(serializers.serialize(value, specifiedType: const FullType(JsonObject))); - } - result.add('html_url'); - if (object.htmlUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.htmlUrl, - specifiedType: const FullType(String))); - } - result.add('submissionStatus'); - if (object.submissionStatus == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionStatus, + value = object.htmlUrl; + + result + ..add('html_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.submissionStatus; + + result + ..add('submissionStatus') + ..add(serializers.serialize(value, specifiedType: const FullType(PlannerSubmission))); - } + return result; } @override - PlannerItem deserialize(Serializers serializers, Iterable serialized, + PlannerItem deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PlannerItemBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'course_id': result.courseId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'context_type': result.contextType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'context_name': result.contextName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'plannable_type': result.plannableType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'plannable': result.plannable.replace(serializers.deserialize(value, - specifiedType: const FullType(Plannable)) as Plannable); + specifiedType: const FullType(Plannable))! as Plannable); break; case 'plannable_date': result.plannableDate = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'submissions': result.submissionStatusRaw = serializers.deserialize(value, - specifiedType: const FullType(JsonObject)) as JsonObject; + specifiedType: const FullType(JsonObject)) as JsonObject?; break; case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'submissionStatus': result.submissionStatus.replace(serializers.deserialize(value, - specifiedType: const FullType(PlannerSubmission)) + specifiedType: const FullType(PlannerSubmission))! as PlannerSubmission); break; } @@ -135,44 +129,42 @@ class _$PlannerItemSerializer implements StructuredSerializer { class _$PlannerItem extends PlannerItem { @override - final String courseId; + final String? courseId; @override - final String contextType; + final String? contextType; @override - final String contextName; + final String? contextName; @override final String plannableType; @override final Plannable plannable; @override - final DateTime plannableDate; + final DateTime? plannableDate; @override - final JsonObject submissionStatusRaw; + final JsonObject? submissionStatusRaw; @override - final String htmlUrl; + final String? htmlUrl; @override - final PlannerSubmission submissionStatus; + final PlannerSubmission? submissionStatus; - factory _$PlannerItem([void Function(PlannerItemBuilder) updates]) => - (new PlannerItemBuilder()..update(updates)).build(); + factory _$PlannerItem([void Function(PlannerItemBuilder)? updates]) => + (new PlannerItemBuilder()..update(updates))._build(); _$PlannerItem._( {this.courseId, this.contextType, this.contextName, - this.plannableType, - this.plannable, + required this.plannableType, + required this.plannable, this.plannableDate, this.submissionStatusRaw, this.htmlUrl, this.submissionStatus}) : super._() { - if (plannableType == null) { - throw new BuiltValueNullFieldError('PlannerItem', 'plannableType'); - } - if (plannable == null) { - throw new BuiltValueNullFieldError('PlannerItem', 'plannable'); - } + BuiltValueNullFieldError.checkNotNull( + plannableType, r'PlannerItem', 'plannableType'); + BuiltValueNullFieldError.checkNotNull( + plannable, r'PlannerItem', 'plannable'); } @override @@ -199,27 +191,23 @@ class _$PlannerItem extends PlannerItem { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc(0, courseId.hashCode), - contextType.hashCode), - contextName.hashCode), - plannableType.hashCode), - plannable.hashCode), - plannableDate.hashCode), - submissionStatusRaw.hashCode), - htmlUrl.hashCode), - submissionStatus.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, courseId.hashCode); + _$hash = $jc(_$hash, contextType.hashCode); + _$hash = $jc(_$hash, contextName.hashCode); + _$hash = $jc(_$hash, plannableType.hashCode); + _$hash = $jc(_$hash, plannable.hashCode); + _$hash = $jc(_$hash, plannableDate.hashCode); + _$hash = $jc(_$hash, submissionStatusRaw.hashCode); + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, submissionStatus.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('PlannerItem') + return (newBuiltValueToStringHelper(r'PlannerItem') ..add('courseId', courseId) ..add('contextType', contextType) ..add('contextName', contextName) @@ -234,63 +222,64 @@ class _$PlannerItem extends PlannerItem { } class PlannerItemBuilder implements Builder { - _$PlannerItem _$v; + _$PlannerItem? _$v; - String _courseId; - String get courseId => _$this._courseId; - set courseId(String courseId) => _$this._courseId = courseId; + String? _courseId; + String? get courseId => _$this._courseId; + set courseId(String? courseId) => _$this._courseId = courseId; - String _contextType; - String get contextType => _$this._contextType; - set contextType(String contextType) => _$this._contextType = contextType; + String? _contextType; + String? get contextType => _$this._contextType; + set contextType(String? contextType) => _$this._contextType = contextType; - String _contextName; - String get contextName => _$this._contextName; - set contextName(String contextName) => _$this._contextName = contextName; + String? _contextName; + String? get contextName => _$this._contextName; + set contextName(String? contextName) => _$this._contextName = contextName; - String _plannableType; - String get plannableType => _$this._plannableType; - set plannableType(String plannableType) => + String? _plannableType; + String? get plannableType => _$this._plannableType; + set plannableType(String? plannableType) => _$this._plannableType = plannableType; - PlannableBuilder _plannable; + PlannableBuilder? _plannable; PlannableBuilder get plannable => _$this._plannable ??= new PlannableBuilder(); - set plannable(PlannableBuilder plannable) => _$this._plannable = plannable; + set plannable(PlannableBuilder? plannable) => _$this._plannable = plannable; - DateTime _plannableDate; - DateTime get plannableDate => _$this._plannableDate; - set plannableDate(DateTime plannableDate) => + DateTime? _plannableDate; + DateTime? get plannableDate => _$this._plannableDate; + set plannableDate(DateTime? plannableDate) => _$this._plannableDate = plannableDate; - JsonObject _submissionStatusRaw; - JsonObject get submissionStatusRaw => _$this._submissionStatusRaw; - set submissionStatusRaw(JsonObject submissionStatusRaw) => + JsonObject? _submissionStatusRaw; + JsonObject? get submissionStatusRaw => _$this._submissionStatusRaw; + set submissionStatusRaw(JsonObject? submissionStatusRaw) => _$this._submissionStatusRaw = submissionStatusRaw; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - PlannerSubmissionBuilder _submissionStatus; + PlannerSubmissionBuilder? _submissionStatus; PlannerSubmissionBuilder get submissionStatus => _$this._submissionStatus ??= new PlannerSubmissionBuilder(); - set submissionStatus(PlannerSubmissionBuilder submissionStatus) => + set submissionStatus(PlannerSubmissionBuilder? submissionStatus) => _$this._submissionStatus = submissionStatus; PlannerItemBuilder(); PlannerItemBuilder get _$this { - if (_$v != null) { - _courseId = _$v.courseId; - _contextType = _$v.contextType; - _contextName = _$v.contextName; - _plannableType = _$v.plannableType; - _plannable = _$v.plannable?.toBuilder(); - _plannableDate = _$v.plannableDate; - _submissionStatusRaw = _$v.submissionStatusRaw; - _htmlUrl = _$v.htmlUrl; - _submissionStatus = _$v.submissionStatus?.toBuilder(); + final $v = _$v; + if ($v != null) { + _courseId = $v.courseId; + _contextType = $v.contextType; + _contextName = $v.contextName; + _plannableType = $v.plannableType; + _plannable = $v.plannable.toBuilder(); + _plannableDate = $v.plannableDate; + _submissionStatusRaw = $v.submissionStatusRaw; + _htmlUrl = $v.htmlUrl; + _submissionStatus = $v.submissionStatus?.toBuilder(); _$v = null; } return this; @@ -298,19 +287,19 @@ class PlannerItemBuilder implements Builder { @override void replace(PlannerItem other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PlannerItem; } @override - void update(void Function(PlannerItemBuilder) updates) { + void update(void Function(PlannerItemBuilder)? updates) { if (updates != null) updates(this); } @override - _$PlannerItem build() { + PlannerItem build() => _build(); + + _$PlannerItem _build() { _$PlannerItem _$result; try { _$result = _$v ?? @@ -318,14 +307,15 @@ class PlannerItemBuilder implements Builder { courseId: courseId, contextType: contextType, contextName: contextName, - plannableType: plannableType, + plannableType: BuiltValueNullFieldError.checkNotNull( + plannableType, r'PlannerItem', 'plannableType'), plannable: plannable.build(), plannableDate: plannableDate, submissionStatusRaw: submissionStatusRaw, htmlUrl: htmlUrl, submissionStatus: _submissionStatus?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'plannable'; plannable.build(); @@ -334,7 +324,7 @@ class PlannerItemBuilder implements Builder { _submissionStatus?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'PlannerItem', _$failedField, e.toString()); + r'PlannerItem', _$failedField, e.toString()); } rethrow; } @@ -343,4 +333,4 @@ class PlannerItemBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/planner_submission.g.dart b/apps/flutter_parent/lib/models/planner_submission.g.dart index fec1862b7c..e12752d243 100644 --- a/apps/flutter_parent/lib/models/planner_submission.g.dart +++ b/apps/flutter_parent/lib/models/planner_submission.g.dart @@ -17,9 +17,9 @@ class _$PlannerSubmissionSerializer final String wireName = 'PlannerSubmission'; @override - Iterable serialize(Serializers serializers, PlannerSubmission object, + Iterable serialize(Serializers serializers, PlannerSubmission object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'submitted', serializers.serialize(object.submitted, specifiedType: const FullType(bool)), @@ -43,40 +43,39 @@ class _$PlannerSubmissionSerializer @override PlannerSubmission deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new PlannerSubmissionBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'submitted': result.submitted = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'excused': result.excused = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'graded': result.graded = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'late': result.late = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'missing': result.missing = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'needs_grading': result.needsGrading = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -100,35 +99,28 @@ class _$PlannerSubmission extends PlannerSubmission { final bool needsGrading; factory _$PlannerSubmission( - [void Function(PlannerSubmissionBuilder) updates]) => - (new PlannerSubmissionBuilder()..update(updates)).build(); + [void Function(PlannerSubmissionBuilder)? updates]) => + (new PlannerSubmissionBuilder()..update(updates))._build(); _$PlannerSubmission._( - {this.submitted, - this.excused, - this.graded, - this.late, - this.missing, - this.needsGrading}) + {required this.submitted, + required this.excused, + required this.graded, + required this.late, + required this.missing, + required this.needsGrading}) : super._() { - if (submitted == null) { - throw new BuiltValueNullFieldError('PlannerSubmission', 'submitted'); - } - if (excused == null) { - throw new BuiltValueNullFieldError('PlannerSubmission', 'excused'); - } - if (graded == null) { - throw new BuiltValueNullFieldError('PlannerSubmission', 'graded'); - } - if (late == null) { - throw new BuiltValueNullFieldError('PlannerSubmission', 'late'); - } - if (missing == null) { - throw new BuiltValueNullFieldError('PlannerSubmission', 'missing'); - } - if (needsGrading == null) { - throw new BuiltValueNullFieldError('PlannerSubmission', 'needsGrading'); - } + BuiltValueNullFieldError.checkNotNull( + submitted, r'PlannerSubmission', 'submitted'); + BuiltValueNullFieldError.checkNotNull( + excused, r'PlannerSubmission', 'excused'); + BuiltValueNullFieldError.checkNotNull( + graded, r'PlannerSubmission', 'graded'); + BuiltValueNullFieldError.checkNotNull(late, r'PlannerSubmission', 'late'); + BuiltValueNullFieldError.checkNotNull( + missing, r'PlannerSubmission', 'missing'); + BuiltValueNullFieldError.checkNotNull( + needsGrading, r'PlannerSubmission', 'needsGrading'); } @override @@ -153,19 +145,20 @@ class _$PlannerSubmission extends PlannerSubmission { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc($jc($jc(0, submitted.hashCode), excused.hashCode), - graded.hashCode), - late.hashCode), - missing.hashCode), - needsGrading.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, submitted.hashCode); + _$hash = $jc(_$hash, excused.hashCode); + _$hash = $jc(_$hash, graded.hashCode); + _$hash = $jc(_$hash, late.hashCode); + _$hash = $jc(_$hash, missing.hashCode); + _$hash = $jc(_$hash, needsGrading.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('PlannerSubmission') + return (newBuiltValueToStringHelper(r'PlannerSubmission') ..add('submitted', submitted) ..add('excused', excused) ..add('graded', graded) @@ -178,44 +171,45 @@ class _$PlannerSubmission extends PlannerSubmission { class PlannerSubmissionBuilder implements Builder { - _$PlannerSubmission _$v; + _$PlannerSubmission? _$v; - bool _submitted; - bool get submitted => _$this._submitted; - set submitted(bool submitted) => _$this._submitted = submitted; + bool? _submitted; + bool? get submitted => _$this._submitted; + set submitted(bool? submitted) => _$this._submitted = submitted; - bool _excused; - bool get excused => _$this._excused; - set excused(bool excused) => _$this._excused = excused; + bool? _excused; + bool? get excused => _$this._excused; + set excused(bool? excused) => _$this._excused = excused; - bool _graded; - bool get graded => _$this._graded; - set graded(bool graded) => _$this._graded = graded; + bool? _graded; + bool? get graded => _$this._graded; + set graded(bool? graded) => _$this._graded = graded; - bool _late; - bool get late => _$this._late; - set late(bool late) => _$this._late = late; + bool? _late; + bool? get late => _$this._late; + set late(bool? late) => _$this._late = late; - bool _missing; - bool get missing => _$this._missing; - set missing(bool missing) => _$this._missing = missing; + bool? _missing; + bool? get missing => _$this._missing; + set missing(bool? missing) => _$this._missing = missing; - bool _needsGrading; - bool get needsGrading => _$this._needsGrading; - set needsGrading(bool needsGrading) => _$this._needsGrading = needsGrading; + bool? _needsGrading; + bool? get needsGrading => _$this._needsGrading; + set needsGrading(bool? needsGrading) => _$this._needsGrading = needsGrading; PlannerSubmissionBuilder() { PlannerSubmission._initializeBuilder(this); } PlannerSubmissionBuilder get _$this { - if (_$v != null) { - _submitted = _$v.submitted; - _excused = _$v.excused; - _graded = _$v.graded; - _late = _$v.late; - _missing = _$v.missing; - _needsGrading = _$v.needsGrading; + final $v = _$v; + if ($v != null) { + _submitted = $v.submitted; + _excused = $v.excused; + _graded = $v.graded; + _late = $v.late; + _missing = $v.missing; + _needsGrading = $v.needsGrading; _$v = null; } return this; @@ -223,30 +217,36 @@ class PlannerSubmissionBuilder @override void replace(PlannerSubmission other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$PlannerSubmission; } @override - void update(void Function(PlannerSubmissionBuilder) updates) { + void update(void Function(PlannerSubmissionBuilder)? updates) { if (updates != null) updates(this); } @override - _$PlannerSubmission build() { + PlannerSubmission build() => _build(); + + _$PlannerSubmission _build() { final _$result = _$v ?? new _$PlannerSubmission._( - submitted: submitted, - excused: excused, - graded: graded, - late: late, - missing: missing, - needsGrading: needsGrading); + submitted: BuiltValueNullFieldError.checkNotNull( + submitted, r'PlannerSubmission', 'submitted'), + excused: BuiltValueNullFieldError.checkNotNull( + excused, r'PlannerSubmission', 'excused'), + graded: BuiltValueNullFieldError.checkNotNull( + graded, r'PlannerSubmission', 'graded'), + late: BuiltValueNullFieldError.checkNotNull( + late, r'PlannerSubmission', 'late'), + missing: BuiltValueNullFieldError.checkNotNull( + missing, r'PlannerSubmission', 'missing'), + needsGrading: BuiltValueNullFieldError.checkNotNull( + needsGrading, r'PlannerSubmission', 'needsGrading')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/recipient.dart b/apps/flutter_parent/lib/models/recipient.dart index 4464132932..c2cc9298dd 100644 --- a/apps/flutter_parent/lib/models/recipient.dart +++ b/apps/flutter_parent/lib/models/recipient.dart @@ -29,16 +29,13 @@ abstract class Recipient implements Built { // The name of the context or short name of the user String get name; - @nullable - String get pronouns; + String? get pronouns; @BuiltValueField(wireName: 'avatar_url') - @nullable - String get avatarUrl; + String? get avatarUrl; @BuiltValueField(wireName: 'common_courses') - @nullable - BuiltMap> get commonCourses; + BuiltMap>? get commonCourses; Recipient._(); factory Recipient([void Function(RecipientBuilder) updates]) = _$Recipient; diff --git a/apps/flutter_parent/lib/models/recipient.g.dart b/apps/flutter_parent/lib/models/recipient.g.dart index 690c91b238..a2d60d192c 100644 --- a/apps/flutter_parent/lib/models/recipient.g.dart +++ b/apps/flutter_parent/lib/models/recipient.g.dart @@ -15,75 +15,73 @@ class _$RecipientSerializer implements StructuredSerializer { final String wireName = 'Recipient'; @override - Iterable serialize(Serializers serializers, Recipient object, + Iterable serialize(Serializers serializers, Recipient object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), ]; - result.add('pronouns'); - if (object.pronouns == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.pronouns, - specifiedType: const FullType(String))); - } - result.add('avatar_url'); - if (object.avatarUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.avatarUrl, - specifiedType: const FullType(String))); - } - result.add('common_courses'); - if (object.commonCourses == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.commonCourses, + Object? value; + value = object.pronouns; + + result + ..add('pronouns') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.avatarUrl; + + result + ..add('avatar_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.commonCourses; + + result + ..add('common_courses') + ..add(serializers.serialize(value, specifiedType: const FullType(BuiltMap, const [ const FullType(String), const FullType(BuiltList, const [const FullType(String)]) ]))); - } + return result; } @override - Recipient deserialize(Serializers serializers, Iterable serialized, + Recipient deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new RecipientBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'pronouns': result.pronouns = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'avatar_url': result.avatarUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'common_courses': result.commonCourses.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltMap, const [ const FullType(String), const FullType(BuiltList, const [const FullType(String)]) - ]))); + ]))!); break; } } @@ -98,24 +96,24 @@ class _$Recipient extends Recipient { @override final String name; @override - final String pronouns; + final String? pronouns; @override - final String avatarUrl; + final String? avatarUrl; @override - final BuiltMap> commonCourses; + final BuiltMap>? commonCourses; - factory _$Recipient([void Function(RecipientBuilder) updates]) => - (new RecipientBuilder()..update(updates)).build(); + factory _$Recipient([void Function(RecipientBuilder)? updates]) => + (new RecipientBuilder()..update(updates))._build(); _$Recipient._( - {this.id, this.name, this.pronouns, this.avatarUrl, this.commonCourses}) + {required this.id, + required this.name, + this.pronouns, + this.avatarUrl, + this.commonCourses}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Recipient', 'id'); - } - if (name == null) { - throw new BuiltValueNullFieldError('Recipient', 'name'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Recipient', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'Recipient', 'name'); } @override @@ -138,15 +136,19 @@ class _$Recipient extends Recipient { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), name.hashCode), pronouns.hashCode), - avatarUrl.hashCode), - commonCourses.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, pronouns.hashCode); + _$hash = $jc(_$hash, avatarUrl.hashCode); + _$hash = $jc(_$hash, commonCourses.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Recipient') + return (newBuiltValueToStringHelper(r'Recipient') ..add('id', id) ..add('name', name) ..add('pronouns', pronouns) @@ -157,39 +159,40 @@ class _$Recipient extends Recipient { } class RecipientBuilder implements Builder { - _$Recipient _$v; + _$Recipient? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _pronouns; - String get pronouns => _$this._pronouns; - set pronouns(String pronouns) => _$this._pronouns = pronouns; + String? _pronouns; + String? get pronouns => _$this._pronouns; + set pronouns(String? pronouns) => _$this._pronouns = pronouns; - String _avatarUrl; - String get avatarUrl => _$this._avatarUrl; - set avatarUrl(String avatarUrl) => _$this._avatarUrl = avatarUrl; + String? _avatarUrl; + String? get avatarUrl => _$this._avatarUrl; + set avatarUrl(String? avatarUrl) => _$this._avatarUrl = avatarUrl; - MapBuilder> _commonCourses; + MapBuilder>? _commonCourses; MapBuilder> get commonCourses => _$this._commonCourses ??= new MapBuilder>(); - set commonCourses(MapBuilder> commonCourses) => + set commonCourses(MapBuilder>? commonCourses) => _$this._commonCourses = commonCourses; RecipientBuilder(); RecipientBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _pronouns = _$v.pronouns; - _avatarUrl = _$v.avatarUrl; - _commonCourses = _$v.commonCourses?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _pronouns = $v.pronouns; + _avatarUrl = $v.avatarUrl; + _commonCourses = $v.commonCourses?.toBuilder(); _$v = null; } return this; @@ -197,36 +200,37 @@ class RecipientBuilder implements Builder { @override void replace(Recipient other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Recipient; } @override - void update(void Function(RecipientBuilder) updates) { + void update(void Function(RecipientBuilder)? updates) { if (updates != null) updates(this); } @override - _$Recipient build() { + Recipient build() => _build(); + + _$Recipient _build() { _$Recipient _$result; try { _$result = _$v ?? new _$Recipient._( - id: id, - name: name, + id: BuiltValueNullFieldError.checkNotNull(id, r'Recipient', 'id'), + name: BuiltValueNullFieldError.checkNotNull( + name, r'Recipient', 'name'), pronouns: pronouns, avatarUrl: avatarUrl, commonCourses: _commonCourses?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'commonCourses'; _commonCourses?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Recipient', _$failedField, e.toString()); + r'Recipient', _$failedField, e.toString()); } rethrow; } @@ -235,4 +239,4 @@ class RecipientBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/reminder.dart b/apps/flutter_parent/lib/models/reminder.dart index a4b4fca2ba..10f6aead33 100644 --- a/apps/flutter_parent/lib/models/reminder.dart +++ b/apps/flutter_parent/lib/models/reminder.dart @@ -30,8 +30,7 @@ abstract class Reminder implements Built { static const TYPE_ASSIGNMENT = 'assignment'; static const TYPE_EVENT = 'event'; - @nullable - int get id; + int? get id; String get userDomain; @@ -43,12 +42,12 @@ abstract class Reminder implements Built { String get courseId; - DateTime get date; + DateTime? get date; Reminder._(); factory Reminder([void Function(ReminderBuilder) updates]) = _$Reminder; - static Reminder fromNotification(NotificationPayload payload) => deserialize(json.decode(payload.data)); + static Reminder? fromNotification(NotificationPayload? payload) => deserialize(json.decode(payload?.data ?? '')); static void _initializeBuilder(ReminderBuilder b) => b ..userDomain = '' diff --git a/apps/flutter_parent/lib/models/reminder.g.dart b/apps/flutter_parent/lib/models/reminder.g.dart index 464ff8b16a..c7c017ff27 100644 --- a/apps/flutter_parent/lib/models/reminder.g.dart +++ b/apps/flutter_parent/lib/models/reminder.g.dart @@ -15,9 +15,9 @@ class _$ReminderSerializer implements StructuredSerializer { final String wireName = 'Reminder'; @override - Iterable serialize(Serializers serializers, Reminder object, + Iterable serialize(Serializers serializers, Reminder object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'userDomain', serializers.serialize(object.userDomain, specifiedType: const FullType(String)), @@ -32,59 +32,61 @@ class _$ReminderSerializer implements StructuredSerializer { 'courseId', serializers.serialize(object.courseId, specifiedType: const FullType(String)), - 'date', - serializers.serialize(object.date, - specifiedType: const FullType(DateTime)), ]; - result.add('id'); - if (object.id == null) { - result.add(null); - } else { - result.add( - serializers.serialize(object.id, specifiedType: const FullType(int))); - } + Object? value; + value = object.id; + + result + ..add('id') + ..add(serializers.serialize(value, specifiedType: const FullType(int))); + value = object.date; + + result + ..add('date') + ..add(serializers.serialize(value, + specifiedType: const FullType(DateTime))); + return result; } @override - Reminder deserialize(Serializers serializers, Iterable serialized, + Reminder deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new ReminderBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int)) as int?; break; case 'userDomain': result.userDomain = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'userId': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'itemId': result.itemId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'courseId': result.courseId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'date': result.date = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; } } @@ -95,7 +97,7 @@ class _$ReminderSerializer implements StructuredSerializer { class _$Reminder extends Reminder { @override - final int id; + final int? id; @override final String userDomain; @override @@ -107,38 +109,26 @@ class _$Reminder extends Reminder { @override final String courseId; @override - final DateTime date; + final DateTime? date; - factory _$Reminder([void Function(ReminderBuilder) updates]) => - (new ReminderBuilder()..update(updates)).build(); + factory _$Reminder([void Function(ReminderBuilder)? updates]) => + (new ReminderBuilder()..update(updates))._build(); _$Reminder._( {this.id, - this.userDomain, - this.userId, - this.type, - this.itemId, - this.courseId, + required this.userDomain, + required this.userId, + required this.type, + required this.itemId, + required this.courseId, this.date}) : super._() { - if (userDomain == null) { - throw new BuiltValueNullFieldError('Reminder', 'userDomain'); - } - if (userId == null) { - throw new BuiltValueNullFieldError('Reminder', 'userId'); - } - if (type == null) { - throw new BuiltValueNullFieldError('Reminder', 'type'); - } - if (itemId == null) { - throw new BuiltValueNullFieldError('Reminder', 'itemId'); - } - if (courseId == null) { - throw new BuiltValueNullFieldError('Reminder', 'courseId'); - } - if (date == null) { - throw new BuiltValueNullFieldError('Reminder', 'date'); - } + BuiltValueNullFieldError.checkNotNull( + userDomain, r'Reminder', 'userDomain'); + BuiltValueNullFieldError.checkNotNull(userId, r'Reminder', 'userId'); + BuiltValueNullFieldError.checkNotNull(type, r'Reminder', 'type'); + BuiltValueNullFieldError.checkNotNull(itemId, r'Reminder', 'itemId'); + BuiltValueNullFieldError.checkNotNull(courseId, r'Reminder', 'courseId'); } @override @@ -163,21 +153,21 @@ class _$Reminder extends Reminder { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), userDomain.hashCode), - userId.hashCode), - type.hashCode), - itemId.hashCode), - courseId.hashCode), - date.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, userDomain.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, itemId.hashCode); + _$hash = $jc(_$hash, courseId.hashCode); + _$hash = $jc(_$hash, date.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Reminder') + return (newBuiltValueToStringHelper(r'Reminder') ..add('id', id) ..add('userDomain', userDomain) ..add('userId', userId) @@ -190,49 +180,50 @@ class _$Reminder extends Reminder { } class ReminderBuilder implements Builder { - _$Reminder _$v; + _$Reminder? _$v; - int _id; - int get id => _$this._id; - set id(int id) => _$this._id = id; + int? _id; + int? get id => _$this._id; + set id(int? id) => _$this._id = id; - String _userDomain; - String get userDomain => _$this._userDomain; - set userDomain(String userDomain) => _$this._userDomain = userDomain; + String? _userDomain; + String? get userDomain => _$this._userDomain; + set userDomain(String? userDomain) => _$this._userDomain = userDomain; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _type; - String get type => _$this._type; - set type(String type) => _$this._type = type; + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; - String _itemId; - String get itemId => _$this._itemId; - set itemId(String itemId) => _$this._itemId = itemId; + String? _itemId; + String? get itemId => _$this._itemId; + set itemId(String? itemId) => _$this._itemId = itemId; - String _courseId; - String get courseId => _$this._courseId; - set courseId(String courseId) => _$this._courseId = courseId; + String? _courseId; + String? get courseId => _$this._courseId; + set courseId(String? courseId) => _$this._courseId = courseId; - DateTime _date; - DateTime get date => _$this._date; - set date(DateTime date) => _$this._date = date; + DateTime? _date; + DateTime? get date => _$this._date; + set date(DateTime? date) => _$this._date = date; ReminderBuilder() { Reminder._initializeBuilder(this); } ReminderBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _userDomain = _$v.userDomain; - _userId = _$v.userId; - _type = _$v.type; - _itemId = _$v.itemId; - _courseId = _$v.courseId; - _date = _$v.date; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _userDomain = $v.userDomain; + _userId = $v.userId; + _type = $v.type; + _itemId = $v.itemId; + _courseId = $v.courseId; + _date = $v.date; _$v = null; } return this; @@ -240,31 +231,36 @@ class ReminderBuilder implements Builder { @override void replace(Reminder other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Reminder; } @override - void update(void Function(ReminderBuilder) updates) { + void update(void Function(ReminderBuilder)? updates) { if (updates != null) updates(this); } @override - _$Reminder build() { + Reminder build() => _build(); + + _$Reminder _build() { final _$result = _$v ?? new _$Reminder._( id: id, - userDomain: userDomain, - userId: userId, - type: type, - itemId: itemId, - courseId: courseId, + userDomain: BuiltValueNullFieldError.checkNotNull( + userDomain, r'Reminder', 'userDomain'), + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'Reminder', 'userId'), + type: BuiltValueNullFieldError.checkNotNull( + type, r'Reminder', 'type'), + itemId: BuiltValueNullFieldError.checkNotNull( + itemId, r'Reminder', 'itemId'), + courseId: BuiltValueNullFieldError.checkNotNull( + courseId, r'Reminder', 'courseId'), date: date); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/remote_file.dart b/apps/flutter_parent/lib/models/remote_file.dart index 38fae9c3c2..6c0a005f0b 100644 --- a/apps/flutter_parent/lib/models/remote_file.dart +++ b/apps/flutter_parent/lib/models/remote_file.dart @@ -34,24 +34,19 @@ abstract class RemoteFile implements Built { String get url; - @nullable - String get filename; + String? get filename; @BuiltValueField(wireName: 'preview_url') - @nullable - String get previewUrl; + String? get previewUrl; @BuiltValueField(wireName: 'thumbnail_url') - @nullable - String get thumbnailUrl; + String? get thumbnailUrl; @BuiltValueField(wireName: 'content-type') - @nullable - String get contentType; + String? get contentType; @BuiltValueField(wireName: 'display_name') - @nullable - String get displayName; + String? get displayName; Attachment toAttachment() { return Attachment((a) => a diff --git a/apps/flutter_parent/lib/models/remote_file.g.dart b/apps/flutter_parent/lib/models/remote_file.g.dart index a1db51f1fa..8b2618fb73 100644 --- a/apps/flutter_parent/lib/models/remote_file.g.dart +++ b/apps/flutter_parent/lib/models/remote_file.g.dart @@ -15,91 +15,87 @@ class _$RemoteFileSerializer implements StructuredSerializer { final String wireName = 'RemoteFile'; @override - Iterable serialize(Serializers serializers, RemoteFile object, + Iterable serialize(Serializers serializers, RemoteFile object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'url', serializers.serialize(object.url, specifiedType: const FullType(String)), ]; - result.add('filename'); - if (object.filename == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.filename, - specifiedType: const FullType(String))); - } - result.add('preview_url'); - if (object.previewUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.previewUrl, - specifiedType: const FullType(String))); - } - result.add('thumbnail_url'); - if (object.thumbnailUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.thumbnailUrl, - specifiedType: const FullType(String))); - } - result.add('content-type'); - if (object.contentType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contentType, - specifiedType: const FullType(String))); - } - result.add('display_name'); - if (object.displayName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.displayName, - specifiedType: const FullType(String))); - } + Object? value; + value = object.filename; + + result + ..add('filename') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.previewUrl; + + result + ..add('preview_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.thumbnailUrl; + + result + ..add('thumbnail_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.contentType; + + result + ..add('content-type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.displayName; + + result + ..add('display_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - RemoteFile deserialize(Serializers serializers, Iterable serialized, + RemoteFile deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new RemoteFileBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'filename': result.filename = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'preview_url': result.previewUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'thumbnail_url': result.thumbnailUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'content-type': result.contentType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'display_name': result.displayName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -114,34 +110,30 @@ class _$RemoteFile extends RemoteFile { @override final String url; @override - final String filename; + final String? filename; @override - final String previewUrl; + final String? previewUrl; @override - final String thumbnailUrl; + final String? thumbnailUrl; @override - final String contentType; + final String? contentType; @override - final String displayName; + final String? displayName; - factory _$RemoteFile([void Function(RemoteFileBuilder) updates]) => - (new RemoteFileBuilder()..update(updates)).build(); + factory _$RemoteFile([void Function(RemoteFileBuilder)? updates]) => + (new RemoteFileBuilder()..update(updates))._build(); _$RemoteFile._( - {this.id, - this.url, + {required this.id, + required this.url, this.filename, this.previewUrl, this.thumbnailUrl, this.contentType, this.displayName}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('RemoteFile', 'id'); - } - if (url == null) { - throw new BuiltValueNullFieldError('RemoteFile', 'url'); - } + BuiltValueNullFieldError.checkNotNull(id, r'RemoteFile', 'id'); + BuiltValueNullFieldError.checkNotNull(url, r'RemoteFile', 'url'); } @override @@ -166,21 +158,21 @@ class _$RemoteFile extends RemoteFile { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), url.hashCode), - filename.hashCode), - previewUrl.hashCode), - thumbnailUrl.hashCode), - contentType.hashCode), - displayName.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, filename.hashCode); + _$hash = $jc(_$hash, previewUrl.hashCode); + _$hash = $jc(_$hash, thumbnailUrl.hashCode); + _$hash = $jc(_$hash, contentType.hashCode); + _$hash = $jc(_$hash, displayName.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('RemoteFile') + return (newBuiltValueToStringHelper(r'RemoteFile') ..add('id', id) ..add('url', url) ..add('filename', filename) @@ -193,49 +185,50 @@ class _$RemoteFile extends RemoteFile { } class RemoteFileBuilder implements Builder { - _$RemoteFile _$v; + _$RemoteFile? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - String _filename; - String get filename => _$this._filename; - set filename(String filename) => _$this._filename = filename; + String? _filename; + String? get filename => _$this._filename; + set filename(String? filename) => _$this._filename = filename; - String _previewUrl; - String get previewUrl => _$this._previewUrl; - set previewUrl(String previewUrl) => _$this._previewUrl = previewUrl; + String? _previewUrl; + String? get previewUrl => _$this._previewUrl; + set previewUrl(String? previewUrl) => _$this._previewUrl = previewUrl; - String _thumbnailUrl; - String get thumbnailUrl => _$this._thumbnailUrl; - set thumbnailUrl(String thumbnailUrl) => _$this._thumbnailUrl = thumbnailUrl; + String? _thumbnailUrl; + String? get thumbnailUrl => _$this._thumbnailUrl; + set thumbnailUrl(String? thumbnailUrl) => _$this._thumbnailUrl = thumbnailUrl; - String _contentType; - String get contentType => _$this._contentType; - set contentType(String contentType) => _$this._contentType = contentType; + String? _contentType; + String? get contentType => _$this._contentType; + set contentType(String? contentType) => _$this._contentType = contentType; - String _displayName; - String get displayName => _$this._displayName; - set displayName(String displayName) => _$this._displayName = displayName; + String? _displayName; + String? get displayName => _$this._displayName; + set displayName(String? displayName) => _$this._displayName = displayName; RemoteFileBuilder() { RemoteFile._initializeBuilder(this); } RemoteFileBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _url = _$v.url; - _filename = _$v.filename; - _previewUrl = _$v.previewUrl; - _thumbnailUrl = _$v.thumbnailUrl; - _contentType = _$v.contentType; - _displayName = _$v.displayName; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _url = $v.url; + _filename = $v.filename; + _previewUrl = $v.previewUrl; + _thumbnailUrl = $v.thumbnailUrl; + _contentType = $v.contentType; + _displayName = $v.displayName; _$v = null; } return this; @@ -243,23 +236,24 @@ class RemoteFileBuilder implements Builder { @override void replace(RemoteFile other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$RemoteFile; } @override - void update(void Function(RemoteFileBuilder) updates) { + void update(void Function(RemoteFileBuilder)? updates) { if (updates != null) updates(this); } @override - _$RemoteFile build() { + RemoteFile build() => _build(); + + _$RemoteFile _build() { final _$result = _$v ?? new _$RemoteFile._( - id: id, - url: url, + id: BuiltValueNullFieldError.checkNotNull(id, r'RemoteFile', 'id'), + url: BuiltValueNullFieldError.checkNotNull( + url, r'RemoteFile', 'url'), filename: filename, previewUrl: previewUrl, thumbnailUrl: thumbnailUrl, @@ -270,4 +264,4 @@ class RemoteFileBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/schedule_item.dart b/apps/flutter_parent/lib/models/schedule_item.dart index e072a94c99..ae9c27793b 100644 --- a/apps/flutter_parent/lib/models/schedule_item.dart +++ b/apps/flutter_parent/lib/models/schedule_item.dart @@ -36,59 +36,46 @@ abstract class ScheduleItem implements Built String get id; - @nullable - String get title; + String? get title; - @nullable - String get description; + String? get description; @BuiltValueField(wireName: 'start_at') - @nullable - DateTime get startAt; + DateTime? get startAt; @BuiltValueField(wireName: 'end_at') - @nullable - DateTime get endAt; + DateTime? get endAt; @BuiltValueField(wireName: 'all_day') bool get isAllDay; @BuiltValueField(wireName: 'all_day_date') - @nullable - DateTime get allDayDate; + DateTime? get allDayDate; @BuiltValueField(wireName: 'location_address') - @nullable - String get locationAddress; + String? get locationAddress; String get type; // Either 'event' or 'assignment' @BuiltValueField(wireName: 'location_name') - @nullable - String get locationName; + String? get locationName; @BuiltValueField(wireName: 'html_url') - @nullable - String get htmlUrl; + String? get htmlUrl; @BuiltValueField(wireName: 'context_code') - @nullable - String get contextCode; + String? get contextCode; @BuiltValueField(wireName: 'effective_context_code') - @nullable - String get effectiveContextCode; + String? get effectiveContextCode; @BuiltValueField(wireName: 'hidden') - @nullable - bool get isHidden; + bool? get isHidden; - @nullable - Assignment get assignment; + Assignment? get assignment; @BuiltValueField(wireName: 'assignment_overrides') - @nullable - BuiltList get assignmentOverrides; + BuiltList? get assignmentOverrides; ScheduleItem._(); factory ScheduleItem([void Function(ScheduleItemBuilder) updates]) = _$ScheduleItem; @@ -121,17 +108,17 @@ abstract class ScheduleItem implements Built String getContextId() { if (effectiveContextCode != null) { - return _parseContextCode(effectiveContextCode); + return _parseContextCode(effectiveContextCode!); } else { - return _parseContextCode(contextCode); + return _parseContextCode(contextCode!); } } String getContextType() { if (effectiveContextCode != null) { - return _parseContextType(effectiveContextCode); + return _parseContextType(effectiveContextCode!); } else { - return _parseContextType(contextCode); + return _parseContextType(contextCode!); } } @@ -145,7 +132,7 @@ abstract class ScheduleItem implements Built return code.substring(0, index); } - PlannerSubmission getPlannerSubmission() { + PlannerSubmission? getPlannerSubmission() { if (assignment == null) return null; // We are only worried about fetching the single submission here, as the calendar request is @@ -161,7 +148,7 @@ abstract class ScheduleItem implements Built ..missing = submission.missing); } - PlannerItem toPlannerItem(String courseName) { + PlannerItem toPlannerItem(String? courseName) { final plannable = Plannable((b) => b ..id = id ..title = title diff --git a/apps/flutter_parent/lib/models/schedule_item.g.dart b/apps/flutter_parent/lib/models/schedule_item.g.dart index fa6113a60a..04fe079965 100644 --- a/apps/flutter_parent/lib/models/schedule_item.g.dart +++ b/apps/flutter_parent/lib/models/schedule_item.g.dart @@ -16,9 +16,9 @@ class _$ScheduleItemSerializer implements StructuredSerializer { final String wireName = 'ScheduleItem'; @override - Iterable serialize(Serializers serializers, ScheduleItem object, + Iterable serialize(Serializers serializers, ScheduleItem object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'all_day', @@ -27,178 +27,166 @@ class _$ScheduleItemSerializer implements StructuredSerializer { 'type', serializers.serialize(object.type, specifiedType: const FullType(String)), ]; - result.add('title'); - if (object.title == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.title, - specifiedType: const FullType(String))); - } - result.add('description'); - if (object.description == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.description, - specifiedType: const FullType(String))); - } - result.add('start_at'); - if (object.startAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.startAt, + Object? value; + value = object.title; + + result + ..add('title') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.description; + + result + ..add('description') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.startAt; + + result + ..add('start_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('end_at'); - if (object.endAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.endAt, + value = object.endAt; + + result + ..add('end_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('all_day_date'); - if (object.allDayDate == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.allDayDate, + value = object.allDayDate; + + result + ..add('all_day_date') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('location_address'); - if (object.locationAddress == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.locationAddress, - specifiedType: const FullType(String))); - } - result.add('location_name'); - if (object.locationName == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.locationName, - specifiedType: const FullType(String))); - } - result.add('html_url'); - if (object.htmlUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.htmlUrl, - specifiedType: const FullType(String))); - } - result.add('context_code'); - if (object.contextCode == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.contextCode, - specifiedType: const FullType(String))); - } - result.add('effective_context_code'); - if (object.effectiveContextCode == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.effectiveContextCode, - specifiedType: const FullType(String))); - } - result.add('hidden'); - if (object.isHidden == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.isHidden, - specifiedType: const FullType(bool))); - } - result.add('assignment'); - if (object.assignment == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.assignment, + value = object.locationAddress; + + result + ..add('location_address') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.locationName; + + result + ..add('location_name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.htmlUrl; + + result + ..add('html_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.contextCode; + + result + ..add('context_code') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.effectiveContextCode; + + result + ..add('effective_context_code') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.isHidden; + + result + ..add('hidden') + ..add(serializers.serialize(value, specifiedType: const FullType(bool))); + value = object.assignment; + + result + ..add('assignment') + ..add(serializers.serialize(value, specifiedType: const FullType(Assignment))); - } - result.add('assignment_overrides'); - if (object.assignmentOverrides == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.assignmentOverrides, + value = object.assignmentOverrides; + + result + ..add('assignment_overrides') + ..add(serializers.serialize(value, specifiedType: const FullType( BuiltList, const [const FullType(AssignmentOverride)]))); - } + return result; } @override - ScheduleItem deserialize(Serializers serializers, Iterable serialized, + ScheduleItem deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new ScheduleItemBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'title': result.title = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'description': result.description = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'start_at': result.startAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'end_at': result.endAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'all_day': result.isAllDay = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'all_day_date': result.allDayDate = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'location_address': result.locationAddress = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'location_name': result.locationName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'html_url': result.htmlUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'context_code': result.contextCode = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'effective_context_code': result.effectiveContextCode = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'hidden': result.isHidden = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool)) as bool?; break; case 'assignment': result.assignment.replace(serializers.deserialize(value, - specifiedType: const FullType(Assignment)) as Assignment); + specifiedType: const FullType(Assignment))! as Assignment); break; case 'assignment_overrides': result.assignmentOverrides.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(AssignmentOverride)])) - as BuiltList); + BuiltList, const [const FullType(AssignmentOverride)]))! + as BuiltList); break; } } @@ -211,49 +199,49 @@ class _$ScheduleItem extends ScheduleItem { @override final String id; @override - final String title; + final String? title; @override - final String description; + final String? description; @override - final DateTime startAt; + final DateTime? startAt; @override - final DateTime endAt; + final DateTime? endAt; @override final bool isAllDay; @override - final DateTime allDayDate; + final DateTime? allDayDate; @override - final String locationAddress; + final String? locationAddress; @override final String type; @override - final String locationName; + final String? locationName; @override - final String htmlUrl; + final String? htmlUrl; @override - final String contextCode; + final String? contextCode; @override - final String effectiveContextCode; + final String? effectiveContextCode; @override - final bool isHidden; + final bool? isHidden; @override - final Assignment assignment; + final Assignment? assignment; @override - final BuiltList assignmentOverrides; + final BuiltList? assignmentOverrides; - factory _$ScheduleItem([void Function(ScheduleItemBuilder) updates]) => - (new ScheduleItemBuilder()..update(updates)).build(); + factory _$ScheduleItem([void Function(ScheduleItemBuilder)? updates]) => + (new ScheduleItemBuilder()..update(updates))._build(); _$ScheduleItem._( - {this.id, + {required this.id, this.title, this.description, this.startAt, this.endAt, - this.isAllDay, + required this.isAllDay, this.allDayDate, this.locationAddress, - this.type, + required this.type, this.locationName, this.htmlUrl, this.contextCode, @@ -262,15 +250,10 @@ class _$ScheduleItem extends ScheduleItem { this.assignment, this.assignmentOverrides}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('ScheduleItem', 'id'); - } - if (isAllDay == null) { - throw new BuiltValueNullFieldError('ScheduleItem', 'isAllDay'); - } - if (type == null) { - throw new BuiltValueNullFieldError('ScheduleItem', 'type'); - } + BuiltValueNullFieldError.checkNotNull(id, r'ScheduleItem', 'id'); + BuiltValueNullFieldError.checkNotNull( + isAllDay, r'ScheduleItem', 'isAllDay'); + BuiltValueNullFieldError.checkNotNull(type, r'ScheduleItem', 'type'); } @override @@ -304,43 +287,30 @@ class _$ScheduleItem extends ScheduleItem { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc(0, - id.hashCode), - title.hashCode), - description.hashCode), - startAt.hashCode), - endAt.hashCode), - isAllDay.hashCode), - allDayDate.hashCode), - locationAddress.hashCode), - type.hashCode), - locationName.hashCode), - htmlUrl.hashCode), - contextCode.hashCode), - effectiveContextCode.hashCode), - isHidden.hashCode), - assignment.hashCode), - assignmentOverrides.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, title.hashCode); + _$hash = $jc(_$hash, description.hashCode); + _$hash = $jc(_$hash, startAt.hashCode); + _$hash = $jc(_$hash, endAt.hashCode); + _$hash = $jc(_$hash, isAllDay.hashCode); + _$hash = $jc(_$hash, allDayDate.hashCode); + _$hash = $jc(_$hash, locationAddress.hashCode); + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, locationName.hashCode); + _$hash = $jc(_$hash, htmlUrl.hashCode); + _$hash = $jc(_$hash, contextCode.hashCode); + _$hash = $jc(_$hash, effectiveContextCode.hashCode); + _$hash = $jc(_$hash, isHidden.hashCode); + _$hash = $jc(_$hash, assignment.hashCode); + _$hash = $jc(_$hash, assignmentOverrides.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('ScheduleItem') + return (newBuiltValueToStringHelper(r'ScheduleItem') ..add('id', id) ..add('title', title) ..add('description', description) @@ -363,77 +333,77 @@ class _$ScheduleItem extends ScheduleItem { class ScheduleItemBuilder implements Builder { - _$ScheduleItem _$v; + _$ScheduleItem? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _title; - String get title => _$this._title; - set title(String title) => _$this._title = title; + String? _title; + String? get title => _$this._title; + set title(String? title) => _$this._title = title; - String _description; - String get description => _$this._description; - set description(String description) => _$this._description = description; + String? _description; + String? get description => _$this._description; + set description(String? description) => _$this._description = description; - DateTime _startAt; - DateTime get startAt => _$this._startAt; - set startAt(DateTime startAt) => _$this._startAt = startAt; + DateTime? _startAt; + DateTime? get startAt => _$this._startAt; + set startAt(DateTime? startAt) => _$this._startAt = startAt; - DateTime _endAt; - DateTime get endAt => _$this._endAt; - set endAt(DateTime endAt) => _$this._endAt = endAt; + DateTime? _endAt; + DateTime? get endAt => _$this._endAt; + set endAt(DateTime? endAt) => _$this._endAt = endAt; - bool _isAllDay; - bool get isAllDay => _$this._isAllDay; - set isAllDay(bool isAllDay) => _$this._isAllDay = isAllDay; + bool? _isAllDay; + bool? get isAllDay => _$this._isAllDay; + set isAllDay(bool? isAllDay) => _$this._isAllDay = isAllDay; - DateTime _allDayDate; - DateTime get allDayDate => _$this._allDayDate; - set allDayDate(DateTime allDayDate) => _$this._allDayDate = allDayDate; + DateTime? _allDayDate; + DateTime? get allDayDate => _$this._allDayDate; + set allDayDate(DateTime? allDayDate) => _$this._allDayDate = allDayDate; - String _locationAddress; - String get locationAddress => _$this._locationAddress; - set locationAddress(String locationAddress) => + String? _locationAddress; + String? get locationAddress => _$this._locationAddress; + set locationAddress(String? locationAddress) => _$this._locationAddress = locationAddress; - String _type; - String get type => _$this._type; - set type(String type) => _$this._type = type; + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; - String _locationName; - String get locationName => _$this._locationName; - set locationName(String locationName) => _$this._locationName = locationName; + String? _locationName; + String? get locationName => _$this._locationName; + set locationName(String? locationName) => _$this._locationName = locationName; - String _htmlUrl; - String get htmlUrl => _$this._htmlUrl; - set htmlUrl(String htmlUrl) => _$this._htmlUrl = htmlUrl; + String? _htmlUrl; + String? get htmlUrl => _$this._htmlUrl; + set htmlUrl(String? htmlUrl) => _$this._htmlUrl = htmlUrl; - String _contextCode; - String get contextCode => _$this._contextCode; - set contextCode(String contextCode) => _$this._contextCode = contextCode; + String? _contextCode; + String? get contextCode => _$this._contextCode; + set contextCode(String? contextCode) => _$this._contextCode = contextCode; - String _effectiveContextCode; - String get effectiveContextCode => _$this._effectiveContextCode; - set effectiveContextCode(String effectiveContextCode) => + String? _effectiveContextCode; + String? get effectiveContextCode => _$this._effectiveContextCode; + set effectiveContextCode(String? effectiveContextCode) => _$this._effectiveContextCode = effectiveContextCode; - bool _isHidden; - bool get isHidden => _$this._isHidden; - set isHidden(bool isHidden) => _$this._isHidden = isHidden; + bool? _isHidden; + bool? get isHidden => _$this._isHidden; + set isHidden(bool? isHidden) => _$this._isHidden = isHidden; - AssignmentBuilder _assignment; + AssignmentBuilder? _assignment; AssignmentBuilder get assignment => _$this._assignment ??= new AssignmentBuilder(); - set assignment(AssignmentBuilder assignment) => + set assignment(AssignmentBuilder? assignment) => _$this._assignment = assignment; - ListBuilder _assignmentOverrides; + ListBuilder? _assignmentOverrides; ListBuilder get assignmentOverrides => _$this._assignmentOverrides ??= new ListBuilder(); set assignmentOverrides( - ListBuilder assignmentOverrides) => + ListBuilder? assignmentOverrides) => _$this._assignmentOverrides = assignmentOverrides; ScheduleItemBuilder() { @@ -441,23 +411,24 @@ class ScheduleItemBuilder } ScheduleItemBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _title = _$v.title; - _description = _$v.description; - _startAt = _$v.startAt; - _endAt = _$v.endAt; - _isAllDay = _$v.isAllDay; - _allDayDate = _$v.allDayDate; - _locationAddress = _$v.locationAddress; - _type = _$v.type; - _locationName = _$v.locationName; - _htmlUrl = _$v.htmlUrl; - _contextCode = _$v.contextCode; - _effectiveContextCode = _$v.effectiveContextCode; - _isHidden = _$v.isHidden; - _assignment = _$v.assignment?.toBuilder(); - _assignmentOverrides = _$v.assignmentOverrides?.toBuilder(); + final $v = _$v; + if ($v != null) { + _id = $v.id; + _title = $v.title; + _description = $v.description; + _startAt = $v.startAt; + _endAt = $v.endAt; + _isAllDay = $v.isAllDay; + _allDayDate = $v.allDayDate; + _locationAddress = $v.locationAddress; + _type = $v.type; + _locationName = $v.locationName; + _htmlUrl = $v.htmlUrl; + _contextCode = $v.contextCode; + _effectiveContextCode = $v.effectiveContextCode; + _isHidden = $v.isHidden; + _assignment = $v.assignment?.toBuilder(); + _assignmentOverrides = $v.assignmentOverrides?.toBuilder(); _$v = null; } return this; @@ -465,32 +436,35 @@ class ScheduleItemBuilder @override void replace(ScheduleItem other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$ScheduleItem; } @override - void update(void Function(ScheduleItemBuilder) updates) { + void update(void Function(ScheduleItemBuilder)? updates) { if (updates != null) updates(this); } @override - _$ScheduleItem build() { + ScheduleItem build() => _build(); + + _$ScheduleItem _build() { _$ScheduleItem _$result; try { _$result = _$v ?? new _$ScheduleItem._( - id: id, + id: BuiltValueNullFieldError.checkNotNull( + id, r'ScheduleItem', 'id'), title: title, description: description, startAt: startAt, endAt: endAt, - isAllDay: isAllDay, + isAllDay: BuiltValueNullFieldError.checkNotNull( + isAllDay, r'ScheduleItem', 'isAllDay'), allDayDate: allDayDate, locationAddress: locationAddress, - type: type, + type: BuiltValueNullFieldError.checkNotNull( + type, r'ScheduleItem', 'type'), locationName: locationName, htmlUrl: htmlUrl, contextCode: contextCode, @@ -499,7 +473,7 @@ class ScheduleItemBuilder assignment: _assignment?.build(), assignmentOverrides: _assignmentOverrides?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'assignment'; _assignment?.build(); @@ -507,7 +481,7 @@ class ScheduleItemBuilder _assignmentOverrides?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'ScheduleItem', _$failedField, e.toString()); + r'ScheduleItem', _$failedField, e.toString()); } rethrow; } @@ -516,4 +490,4 @@ class ScheduleItemBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/school_domain.dart b/apps/flutter_parent/lib/models/school_domain.dart index 16ab1b585b..82013557c4 100644 --- a/apps/flutter_parent/lib/models/school_domain.dart +++ b/apps/flutter_parent/lib/models/school_domain.dart @@ -24,11 +24,10 @@ abstract class SchoolDomain implements Built static Serializer get serializer => _$schoolDomainSerializer; String get domain; - String get name; + String? get name; - @nullable @BuiltValueField(wireName: 'authentication_provider') - String get authenticationProvider; + String? get authenticationProvider; SchoolDomain._(); factory SchoolDomain([void Function(SchoolDomainBuilder) updates]) = _$SchoolDomain; diff --git a/apps/flutter_parent/lib/models/school_domain.g.dart b/apps/flutter_parent/lib/models/school_domain.g.dart index cf48b4b087..18b1e3378f 100644 --- a/apps/flutter_parent/lib/models/school_domain.g.dart +++ b/apps/flutter_parent/lib/models/school_domain.g.dart @@ -16,48 +16,53 @@ class _$SchoolDomainSerializer implements StructuredSerializer { final String wireName = 'SchoolDomain'; @override - Iterable serialize(Serializers serializers, SchoolDomain object, + Iterable serialize(Serializers serializers, SchoolDomain object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'domain', serializers.serialize(object.domain, specifiedType: const FullType(String)), - 'name', - serializers.serialize(object.name, specifiedType: const FullType(String)), ]; - result.add('authentication_provider'); - if (object.authenticationProvider == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.authenticationProvider, - specifiedType: const FullType(String))); - } + Object? value; + value = object.name; + + result + ..add('name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.authenticationProvider; + + result + ..add('authentication_provider') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override - SchoolDomain deserialize(Serializers serializers, Iterable serialized, + SchoolDomain deserialize( + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new SchoolDomainBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'domain': result.domain = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'authentication_provider': result.authenticationProvider = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -70,21 +75,17 @@ class _$SchoolDomain extends SchoolDomain { @override final String domain; @override - final String name; + final String? name; @override - final String authenticationProvider; + final String? authenticationProvider; - factory _$SchoolDomain([void Function(SchoolDomainBuilder) updates]) => - (new SchoolDomainBuilder()..update(updates)).build(); + factory _$SchoolDomain([void Function(SchoolDomainBuilder)? updates]) => + (new SchoolDomainBuilder()..update(updates))._build(); - _$SchoolDomain._({this.domain, this.name, this.authenticationProvider}) + _$SchoolDomain._( + {required this.domain, this.name, this.authenticationProvider}) : super._() { - if (domain == null) { - throw new BuiltValueNullFieldError('SchoolDomain', 'domain'); - } - if (name == null) { - throw new BuiltValueNullFieldError('SchoolDomain', 'name'); - } + BuiltValueNullFieldError.checkNotNull(domain, r'SchoolDomain', 'domain'); } @override @@ -105,13 +106,17 @@ class _$SchoolDomain extends SchoolDomain { @override int get hashCode { - return $jf($jc($jc($jc(0, domain.hashCode), name.hashCode), - authenticationProvider.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, domain.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, authenticationProvider.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('SchoolDomain') + return (newBuiltValueToStringHelper(r'SchoolDomain') ..add('domain', domain) ..add('name', name) ..add('authenticationProvider', authenticationProvider)) @@ -121,28 +126,29 @@ class _$SchoolDomain extends SchoolDomain { class SchoolDomainBuilder implements Builder { - _$SchoolDomain _$v; + _$SchoolDomain? _$v; - String _domain; - String get domain => _$this._domain; - set domain(String domain) => _$this._domain = domain; + String? _domain; + String? get domain => _$this._domain; + set domain(String? domain) => _$this._domain = domain; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _authenticationProvider; - String get authenticationProvider => _$this._authenticationProvider; - set authenticationProvider(String authenticationProvider) => + String? _authenticationProvider; + String? get authenticationProvider => _$this._authenticationProvider; + set authenticationProvider(String? authenticationProvider) => _$this._authenticationProvider = authenticationProvider; SchoolDomainBuilder(); SchoolDomainBuilder get _$this { - if (_$v != null) { - _domain = _$v.domain; - _name = _$v.name; - _authenticationProvider = _$v.authenticationProvider; + final $v = _$v; + if ($v != null) { + _domain = $v.domain; + _name = $v.name; + _authenticationProvider = $v.authenticationProvider; _$v = null; } return this; @@ -150,22 +156,23 @@ class SchoolDomainBuilder @override void replace(SchoolDomain other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$SchoolDomain; } @override - void update(void Function(SchoolDomainBuilder) updates) { + void update(void Function(SchoolDomainBuilder)? updates) { if (updates != null) updates(this); } @override - _$SchoolDomain build() { + SchoolDomain build() => _build(); + + _$SchoolDomain _build() { final _$result = _$v ?? new _$SchoolDomain._( - domain: domain, + domain: BuiltValueNullFieldError.checkNotNull( + domain, r'SchoolDomain', 'domain'), name: name, authenticationProvider: authenticationProvider); replace(_$result); @@ -173,4 +180,4 @@ class SchoolDomainBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/section.dart b/apps/flutter_parent/lib/models/section.dart index 93ca8ea030..013afdc22a 100644 --- a/apps/flutter_parent/lib/models/section.dart +++ b/apps/flutter_parent/lib/models/section.dart @@ -29,12 +29,10 @@ abstract class Section implements Built { String get name; @BuiltValueField(wireName: 'start_at') - @nullable - DateTime get startAt; + DateTime? get startAt; @BuiltValueField(wireName: 'end_at') - @nullable - DateTime get endAt; + DateTime? get endAt; Section._(); factory Section([void Function(SectionBuilder) updates]) = _$Section; diff --git a/apps/flutter_parent/lib/models/section.g.dart b/apps/flutter_parent/lib/models/section.g.dart index 791df9862d..fe5f08729a 100644 --- a/apps/flutter_parent/lib/models/section.g.dart +++ b/apps/flutter_parent/lib/models/section.g.dart @@ -15,58 +15,57 @@ class _$SectionSerializer implements StructuredSerializer
{ final String wireName = 'Section'; @override - Iterable serialize(Serializers serializers, Section object, + Iterable serialize(Serializers serializers, Section object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), ]; - result.add('start_at'); - if (object.startAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.startAt, + Object? value; + value = object.startAt; + + result + ..add('start_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('end_at'); - if (object.endAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.endAt, + value = object.endAt; + + result + ..add('end_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override - Section deserialize(Serializers serializers, Iterable serialized, + Section deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new SectionBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'start_at': result.startAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'end_at': result.endAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; } } @@ -81,20 +80,17 @@ class _$Section extends Section { @override final String name; @override - final DateTime startAt; + final DateTime? startAt; @override - final DateTime endAt; + final DateTime? endAt; - factory _$Section([void Function(SectionBuilder) updates]) => - (new SectionBuilder()..update(updates)).build(); + factory _$Section([void Function(SectionBuilder)? updates]) => + (new SectionBuilder()..update(updates))._build(); - _$Section._({this.id, this.name, this.startAt, this.endAt}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Section', 'id'); - } - if (name == null) { - throw new BuiltValueNullFieldError('Section', 'name'); - } + _$Section._({required this.id, required this.name, this.startAt, this.endAt}) + : super._() { + BuiltValueNullFieldError.checkNotNull(id, r'Section', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'Section', 'name'); } @override @@ -116,14 +112,18 @@ class _$Section extends Section { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, id.hashCode), name.hashCode), startAt.hashCode), - endAt.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, startAt.hashCode); + _$hash = $jc(_$hash, endAt.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Section') + return (newBuiltValueToStringHelper(r'Section') ..add('id', id) ..add('name', name) ..add('startAt', startAt) @@ -133,34 +133,35 @@ class _$Section extends Section { } class SectionBuilder implements Builder { - _$Section _$v; + _$Section? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - DateTime _startAt; - DateTime get startAt => _$this._startAt; - set startAt(DateTime startAt) => _$this._startAt = startAt; + DateTime? _startAt; + DateTime? get startAt => _$this._startAt; + set startAt(DateTime? startAt) => _$this._startAt = startAt; - DateTime _endAt; - DateTime get endAt => _$this._endAt; - set endAt(DateTime endAt) => _$this._endAt = endAt; + DateTime? _endAt; + DateTime? get endAt => _$this._endAt; + set endAt(DateTime? endAt) => _$this._endAt = endAt; SectionBuilder() { Section._initializeBuilder(this); } SectionBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _startAt = _$v.startAt; - _endAt = _$v.endAt; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _startAt = $v.startAt; + _endAt = $v.endAt; _$v = null; } return this; @@ -168,24 +169,29 @@ class SectionBuilder implements Builder { @override void replace(Section other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Section; } @override - void update(void Function(SectionBuilder) updates) { + void update(void Function(SectionBuilder)? updates) { if (updates != null) updates(this); } @override - _$Section build() { + Section build() => _build(); + + _$Section _build() { final _$result = _$v ?? - new _$Section._(id: id, name: name, startAt: startAt, endAt: endAt); + new _$Section._( + id: BuiltValueNullFieldError.checkNotNull(id, r'Section', 'id'), + name: + BuiltValueNullFieldError.checkNotNull(name, r'Section', 'name'), + startAt: startAt, + endAt: endAt); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/serializers.dart b/apps/flutter_parent/lib/models/serializers.dart index 1b3c08e964..cd7d80f6bd 100644 --- a/apps/flutter_parent/lib/models/serializers.dart +++ b/apps/flutter_parent/lib/models/serializers.dart @@ -193,25 +193,33 @@ Serializers jsonSerializers = (_serializers.toBuilder() ..addBuilderFactory(FullType(BuiltMap, [FullType(String), FullType(String)]), () => MapBuilder())) .build(); -T deserialize(dynamic value) => jsonSerializers.deserializeWith(jsonSerializers.serializerForType(T), value); +T? deserialize(dynamic value) { + var serializer = jsonSerializers.serializerForType(T); + if (serializer == null || !(serializer is Serializer)) return null; + return jsonSerializers.deserializeWith(serializer, value); +} -dynamic serialize(T value) => jsonSerializers.serializeWith(jsonSerializers.serializerForType(T), value); +dynamic serialize(T value) { + var serializer = jsonSerializers.serializerForType(T); + if (serializer == null) return null; + return jsonSerializers.serializeWith(serializer, value); +} List deserializeList(dynamic value) => List.from(value?.map((value) => deserialize(value))?.toList() ?? []); /// Plugin that works around an issue where deserialization breaks if a map key is null /// Sourced from https://github.com/google/built_value.dart/issues/653#issuecomment-495964030 class RemoveNullInMapConvertedListPlugin implements SerializerPlugin { - Object beforeSerialize(Object object, FullType specifiedType) => object; + Object? beforeSerialize(Object? object, FullType specifiedType) => object; - Object afterSerialize(Object object, FullType specifiedType) => object; + Object? afterSerialize(Object? object, FullType specifiedType) => object; - Object beforeDeserialize(Object object, FullType specifiedType) { + Object? beforeDeserialize(Object? object, FullType specifiedType) { if (specifiedType.root == BuiltMap && object is List) { return object.where((v) => v != null).toList(); } return object; } - Object afterDeserialize(Object object, FullType specifiedType) => object; + Object? afterDeserialize(Object? object, FullType specifiedType) => object; } diff --git a/apps/flutter_parent/lib/models/serializers.g.dart b/apps/flutter_parent/lib/models/serializers.g.dart index 0806afda58..0fd8e996cf 100644 --- a/apps/flutter_parent/lib/models/serializers.g.dart +++ b/apps/flutter_parent/lib/models/serializers.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of serializers; +part of 'serializers.dart'; // ************************************************************************** // BuiltValueGenerator @@ -174,4 +174,4 @@ Serializers _$_serializers = (new Serializers().toBuilder() () => new MapBuilder())) .build(); -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/snicker_doodle.dart b/apps/flutter_parent/lib/models/snicker_doodle.dart index 4f35802d3c..fd81859591 100644 --- a/apps/flutter_parent/lib/models/snicker_doodle.dart +++ b/apps/flutter_parent/lib/models/snicker_doodle.dart @@ -16,11 +16,11 @@ import 'package:flutter/foundation.dart'; class SnickerDoodle { const SnickerDoodle({ - @required this.title, - @required this.subtitle, - @required this.username, - @required this.password, - @required this.domain, + required this.title, + required this.subtitle, + required this.username, + required this.password, + required this.domain, }); final String title; diff --git a/apps/flutter_parent/lib/models/submission.dart b/apps/flutter_parent/lib/models/submission.dart index fa6513715f..ec1eb1fe59 100644 --- a/apps/flutter_parent/lib/models/submission.dart +++ b/apps/flutter_parent/lib/models/submission.dart @@ -32,52 +32,41 @@ abstract class Submission implements Built { String get id; - @nullable - String get grade; + String? get grade; double get score; int get attempt; - @nullable @BuiltValueField(wireName: 'submitted_at') - DateTime get submittedAt; + DateTime? get submittedAt; - @nullable - DateTime get commentCreated; + DateTime? get commentCreated; - @nullable - String get mediaContentType; + String? get mediaContentType; - @nullable - String get mediaCommentUrl; + String? get mediaCommentUrl; - @nullable - String get mediaCommentDisplay; + String? get mediaCommentDisplay; @BuiltValueField(wireName: 'submission_history') BuiltList get submissionHistory; - @nullable - String get body; + String? get body; @BuiltValueField(wireName: 'grade_matches_current_submission') bool get isGradeMatchesCurrentSubmission; - @nullable @BuiltValueField(wireName: 'workflow_state') - String get workflowState; + String? get workflowState; - @nullable @BuiltValueField(wireName: 'submission_type') - String get submissionType; + String? get submissionType; - @nullable @BuiltValueField(wireName: 'preview_url') - String get previewUrl; + String? get previewUrl; - @nullable - String get url; + String? get url; // Not sure why, but build_runner fails when this field is named 'late' @BuiltValueField(wireName: 'late') @@ -91,8 +80,7 @@ abstract class Submission implements Built { @BuiltValueField(wireName: 'assignment_id') String get assignmentId; - @nullable - Assignment get assignment; + Assignment? get assignment; @BuiltValueField(wireName: 'user_id') String get userId; @@ -100,23 +88,19 @@ abstract class Submission implements Built { @BuiltValueField(wireName: 'grader_id') String get graderId; - @nullable - User get user; + User? get user; - @nullable @BuiltValueField(wireName: 'points_deducted') - double get pointsDeducted; + double? get pointsDeducted; @BuiltValueField(wireName: 'entered_score') double get enteredScore; - @nullable @BuiltValueField(wireName: 'entered_grade') - String get enteredGrade; + String? get enteredGrade; - @nullable @BuiltValueField(wireName: 'posted_at') - DateTime get postedAt; + DateTime? get postedAt; bool isGraded() { return grade != null && workflowState != 'pending_review' && postedAt != null; diff --git a/apps/flutter_parent/lib/models/submission.g.dart b/apps/flutter_parent/lib/models/submission.g.dart index 3bd38bacff..ec4eff6315 100644 --- a/apps/flutter_parent/lib/models/submission.g.dart +++ b/apps/flutter_parent/lib/models/submission.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of submission; +part of 'submission.dart'; // ************************************************************************** // BuiltValueGenerator @@ -15,9 +15,9 @@ class _$SubmissionSerializer implements StructuredSerializer { final String wireName = 'Submission'; @override - Iterable serialize(Serializers serializers, Submission object, + Iterable serialize(Serializers serializers, Submission object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'score', @@ -53,246 +53,230 @@ class _$SubmissionSerializer implements StructuredSerializer { serializers.serialize(object.enteredScore, specifiedType: const FullType(double)), ]; - result.add('grade'); - if (object.grade == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.grade, - specifiedType: const FullType(String))); - } - result.add('submitted_at'); - if (object.submittedAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submittedAt, + Object? value; + value = object.grade; + + result + ..add('grade') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.submittedAt; + + result + ..add('submitted_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('commentCreated'); - if (object.commentCreated == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.commentCreated, + value = object.commentCreated; + + result + ..add('commentCreated') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('mediaContentType'); - if (object.mediaContentType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.mediaContentType, - specifiedType: const FullType(String))); - } - result.add('mediaCommentUrl'); - if (object.mediaCommentUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.mediaCommentUrl, - specifiedType: const FullType(String))); - } - result.add('mediaCommentDisplay'); - if (object.mediaCommentDisplay == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.mediaCommentDisplay, - specifiedType: const FullType(String))); - } - result.add('body'); - if (object.body == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.body, - specifiedType: const FullType(String))); - } - result.add('workflow_state'); - if (object.workflowState == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.workflowState, - specifiedType: const FullType(String))); - } - result.add('submission_type'); - if (object.submissionType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.submissionType, - specifiedType: const FullType(String))); - } - result.add('preview_url'); - if (object.previewUrl == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.previewUrl, - specifiedType: const FullType(String))); - } - result.add('url'); - if (object.url == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.url, - specifiedType: const FullType(String))); - } - result.add('assignment'); - if (object.assignment == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.assignment, + value = object.mediaContentType; + + result + ..add('mediaContentType') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.mediaCommentUrl; + + result + ..add('mediaCommentUrl') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.mediaCommentDisplay; + + result + ..add('mediaCommentDisplay') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.body; + + result + ..add('body') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.workflowState; + + result + ..add('workflow_state') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.submissionType; + + result + ..add('submission_type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.previewUrl; + + result + ..add('preview_url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.url; + + result + ..add('url') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.assignment; + + result + ..add('assignment') + ..add(serializers.serialize(value, specifiedType: const FullType(Assignment))); - } - result.add('user'); - if (object.user == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.user, - specifiedType: const FullType(User))); - } - result.add('points_deducted'); - if (object.pointsDeducted == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.pointsDeducted, - specifiedType: const FullType(double))); - } - result.add('entered_grade'); - if (object.enteredGrade == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.enteredGrade, - specifiedType: const FullType(String))); - } - result.add('posted_at'); - if (object.postedAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.postedAt, + value = object.user; + + result + ..add('user') + ..add(serializers.serialize(value, specifiedType: const FullType(User))); + value = object.pointsDeducted; + + result + ..add('points_deducted') + ..add( + serializers.serialize(value, specifiedType: const FullType(double))); + value = object.enteredGrade; + + result + ..add('entered_grade') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.postedAt; + + result + ..add('posted_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override - Submission deserialize(Serializers serializers, Iterable serialized, + Submission deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new SubmissionBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'grade': result.grade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'score': result.score = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double))! as double; break; case 'attempt': result.attempt = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; case 'submitted_at': result.submittedAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'commentCreated': result.commentCreated = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'mediaContentType': result.mediaContentType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'mediaCommentUrl': result.mediaCommentUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'mediaCommentDisplay': result.mediaCommentDisplay = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'submission_history': result.submissionHistory.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(Submission)])) - as BuiltList); + BuiltList, const [const FullType(Submission)]))! + as BuiltList); break; case 'body': result.body = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'grade_matches_current_submission': result.isGradeMatchesCurrentSubmission = serializers - .deserialize(value, specifiedType: const FullType(bool)) as bool; + .deserialize(value, specifiedType: const FullType(bool))! as bool; break; case 'workflow_state': result.workflowState = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'submission_type': result.submissionType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'preview_url': result.previewUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'url': result.url = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'late': result.isLate = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'excused': result.excused = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'missing': result.missing = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'assignment_id': result.assignmentId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'assignment': result.assignment.replace(serializers.deserialize(value, - specifiedType: const FullType(Assignment)) as Assignment); + specifiedType: const FullType(Assignment))! as Assignment); break; case 'user_id': result.userId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'grader_id': result.graderId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'user': result.user.replace(serializers.deserialize(value, - specifiedType: const FullType(User)) as User); + specifiedType: const FullType(User))! as User); break; case 'points_deducted': result.pointsDeducted = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double)) as double?; break; case 'entered_score': result.enteredScore = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; + specifiedType: const FullType(double))! as double; break; case 'entered_grade': result.enteredGrade = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'posted_at': result.postedAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; } } @@ -305,35 +289,35 @@ class _$Submission extends Submission { @override final String id; @override - final String grade; + final String? grade; @override final double score; @override final int attempt; @override - final DateTime submittedAt; + final DateTime? submittedAt; @override - final DateTime commentCreated; + final DateTime? commentCreated; @override - final String mediaContentType; + final String? mediaContentType; @override - final String mediaCommentUrl; + final String? mediaCommentUrl; @override - final String mediaCommentDisplay; + final String? mediaCommentDisplay; @override final BuiltList submissionHistory; @override - final String body; + final String? body; @override final bool isGradeMatchesCurrentSubmission; @override - final String workflowState; + final String? workflowState; @override - final String submissionType; + final String? submissionType; @override - final String previewUrl; + final String? previewUrl; @override - final String url; + final String? url; @override final bool isLate; @override @@ -343,92 +327,71 @@ class _$Submission extends Submission { @override final String assignmentId; @override - final Assignment assignment; + final Assignment? assignment; @override final String userId; @override final String graderId; @override - final User user; + final User? user; @override - final double pointsDeducted; + final double? pointsDeducted; @override final double enteredScore; @override - final String enteredGrade; + final String? enteredGrade; @override - final DateTime postedAt; + final DateTime? postedAt; - factory _$Submission([void Function(SubmissionBuilder) updates]) => - (new SubmissionBuilder()..update(updates)).build(); + factory _$Submission([void Function(SubmissionBuilder)? updates]) => + (new SubmissionBuilder()..update(updates))._build(); _$Submission._( - {this.id, + {required this.id, this.grade, - this.score, - this.attempt, + required this.score, + required this.attempt, this.submittedAt, this.commentCreated, this.mediaContentType, this.mediaCommentUrl, this.mediaCommentDisplay, - this.submissionHistory, + required this.submissionHistory, this.body, - this.isGradeMatchesCurrentSubmission, + required this.isGradeMatchesCurrentSubmission, this.workflowState, this.submissionType, this.previewUrl, this.url, - this.isLate, - this.excused, - this.missing, - this.assignmentId, + required this.isLate, + required this.excused, + required this.missing, + required this.assignmentId, this.assignment, - this.userId, - this.graderId, + required this.userId, + required this.graderId, this.user, this.pointsDeducted, - this.enteredScore, + required this.enteredScore, this.enteredGrade, this.postedAt}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Submission', 'id'); - } - if (score == null) { - throw new BuiltValueNullFieldError('Submission', 'score'); - } - if (attempt == null) { - throw new BuiltValueNullFieldError('Submission', 'attempt'); - } - if (submissionHistory == null) { - throw new BuiltValueNullFieldError('Submission', 'submissionHistory'); - } - if (isGradeMatchesCurrentSubmission == null) { - throw new BuiltValueNullFieldError( - 'Submission', 'isGradeMatchesCurrentSubmission'); - } - if (isLate == null) { - throw new BuiltValueNullFieldError('Submission', 'isLate'); - } - if (excused == null) { - throw new BuiltValueNullFieldError('Submission', 'excused'); - } - if (missing == null) { - throw new BuiltValueNullFieldError('Submission', 'missing'); - } - if (assignmentId == null) { - throw new BuiltValueNullFieldError('Submission', 'assignmentId'); - } - if (userId == null) { - throw new BuiltValueNullFieldError('Submission', 'userId'); - } - if (graderId == null) { - throw new BuiltValueNullFieldError('Submission', 'graderId'); - } - if (enteredScore == null) { - throw new BuiltValueNullFieldError('Submission', 'enteredScore'); - } + BuiltValueNullFieldError.checkNotNull(id, r'Submission', 'id'); + BuiltValueNullFieldError.checkNotNull(score, r'Submission', 'score'); + BuiltValueNullFieldError.checkNotNull(attempt, r'Submission', 'attempt'); + BuiltValueNullFieldError.checkNotNull( + submissionHistory, r'Submission', 'submissionHistory'); + BuiltValueNullFieldError.checkNotNull(isGradeMatchesCurrentSubmission, + r'Submission', 'isGradeMatchesCurrentSubmission'); + BuiltValueNullFieldError.checkNotNull(isLate, r'Submission', 'isLate'); + BuiltValueNullFieldError.checkNotNull(excused, r'Submission', 'excused'); + BuiltValueNullFieldError.checkNotNull(missing, r'Submission', 'missing'); + BuiltValueNullFieldError.checkNotNull( + assignmentId, r'Submission', 'assignmentId'); + BuiltValueNullFieldError.checkNotNull(userId, r'Submission', 'userId'); + BuiltValueNullFieldError.checkNotNull(graderId, r'Submission', 'graderId'); + BuiltValueNullFieldError.checkNotNull( + enteredScore, r'Submission', 'enteredScore'); } @override @@ -475,49 +438,42 @@ class _$Submission extends Submission { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, id.hashCode), grade.hashCode), score.hashCode), attempt.hashCode), submittedAt.hashCode), commentCreated.hashCode), mediaContentType.hashCode), mediaCommentUrl.hashCode), mediaCommentDisplay.hashCode), - submissionHistory.hashCode), - body.hashCode), - isGradeMatchesCurrentSubmission.hashCode), - workflowState.hashCode), - submissionType.hashCode), - previewUrl.hashCode), - url.hashCode), - isLate.hashCode), - excused.hashCode), - missing.hashCode), - assignmentId.hashCode), - assignment.hashCode), - userId.hashCode), - graderId.hashCode), - user.hashCode), - pointsDeducted.hashCode), - enteredScore.hashCode), - enteredGrade.hashCode), - postedAt.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, grade.hashCode); + _$hash = $jc(_$hash, score.hashCode); + _$hash = $jc(_$hash, attempt.hashCode); + _$hash = $jc(_$hash, submittedAt.hashCode); + _$hash = $jc(_$hash, commentCreated.hashCode); + _$hash = $jc(_$hash, mediaContentType.hashCode); + _$hash = $jc(_$hash, mediaCommentUrl.hashCode); + _$hash = $jc(_$hash, mediaCommentDisplay.hashCode); + _$hash = $jc(_$hash, submissionHistory.hashCode); + _$hash = $jc(_$hash, body.hashCode); + _$hash = $jc(_$hash, isGradeMatchesCurrentSubmission.hashCode); + _$hash = $jc(_$hash, workflowState.hashCode); + _$hash = $jc(_$hash, submissionType.hashCode); + _$hash = $jc(_$hash, previewUrl.hashCode); + _$hash = $jc(_$hash, url.hashCode); + _$hash = $jc(_$hash, isLate.hashCode); + _$hash = $jc(_$hash, excused.hashCode); + _$hash = $jc(_$hash, missing.hashCode); + _$hash = $jc(_$hash, assignmentId.hashCode); + _$hash = $jc(_$hash, assignment.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, graderId.hashCode); + _$hash = $jc(_$hash, user.hashCode); + _$hash = $jc(_$hash, pointsDeducted.hashCode); + _$hash = $jc(_$hash, enteredScore.hashCode); + _$hash = $jc(_$hash, enteredGrade.hashCode); + _$hash = $jc(_$hash, postedAt.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Submission') + return (newBuiltValueToStringHelper(r'Submission') ..add('id', id) ..add('grade', grade) ..add('score', score) @@ -552,167 +508,168 @@ class _$Submission extends Submission { } class SubmissionBuilder implements Builder { - _$Submission _$v; + _$Submission? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _grade; - String get grade => _$this._grade; - set grade(String grade) => _$this._grade = grade; + String? _grade; + String? get grade => _$this._grade; + set grade(String? grade) => _$this._grade = grade; - double _score; - double get score => _$this._score; - set score(double score) => _$this._score = score; + double? _score; + double? get score => _$this._score; + set score(double? score) => _$this._score = score; - int _attempt; - int get attempt => _$this._attempt; - set attempt(int attempt) => _$this._attempt = attempt; + int? _attempt; + int? get attempt => _$this._attempt; + set attempt(int? attempt) => _$this._attempt = attempt; - DateTime _submittedAt; - DateTime get submittedAt => _$this._submittedAt; - set submittedAt(DateTime submittedAt) => _$this._submittedAt = submittedAt; + DateTime? _submittedAt; + DateTime? get submittedAt => _$this._submittedAt; + set submittedAt(DateTime? submittedAt) => _$this._submittedAt = submittedAt; - DateTime _commentCreated; - DateTime get commentCreated => _$this._commentCreated; - set commentCreated(DateTime commentCreated) => + DateTime? _commentCreated; + DateTime? get commentCreated => _$this._commentCreated; + set commentCreated(DateTime? commentCreated) => _$this._commentCreated = commentCreated; - String _mediaContentType; - String get mediaContentType => _$this._mediaContentType; - set mediaContentType(String mediaContentType) => + String? _mediaContentType; + String? get mediaContentType => _$this._mediaContentType; + set mediaContentType(String? mediaContentType) => _$this._mediaContentType = mediaContentType; - String _mediaCommentUrl; - String get mediaCommentUrl => _$this._mediaCommentUrl; - set mediaCommentUrl(String mediaCommentUrl) => + String? _mediaCommentUrl; + String? get mediaCommentUrl => _$this._mediaCommentUrl; + set mediaCommentUrl(String? mediaCommentUrl) => _$this._mediaCommentUrl = mediaCommentUrl; - String _mediaCommentDisplay; - String get mediaCommentDisplay => _$this._mediaCommentDisplay; - set mediaCommentDisplay(String mediaCommentDisplay) => + String? _mediaCommentDisplay; + String? get mediaCommentDisplay => _$this._mediaCommentDisplay; + set mediaCommentDisplay(String? mediaCommentDisplay) => _$this._mediaCommentDisplay = mediaCommentDisplay; - ListBuilder _submissionHistory; + ListBuilder? _submissionHistory; ListBuilder get submissionHistory => _$this._submissionHistory ??= new ListBuilder(); - set submissionHistory(ListBuilder submissionHistory) => + set submissionHistory(ListBuilder? submissionHistory) => _$this._submissionHistory = submissionHistory; - String _body; - String get body => _$this._body; - set body(String body) => _$this._body = body; + String? _body; + String? get body => _$this._body; + set body(String? body) => _$this._body = body; - bool _isGradeMatchesCurrentSubmission; - bool get isGradeMatchesCurrentSubmission => + bool? _isGradeMatchesCurrentSubmission; + bool? get isGradeMatchesCurrentSubmission => _$this._isGradeMatchesCurrentSubmission; - set isGradeMatchesCurrentSubmission(bool isGradeMatchesCurrentSubmission) => + set isGradeMatchesCurrentSubmission(bool? isGradeMatchesCurrentSubmission) => _$this._isGradeMatchesCurrentSubmission = isGradeMatchesCurrentSubmission; - String _workflowState; - String get workflowState => _$this._workflowState; - set workflowState(String workflowState) => + String? _workflowState; + String? get workflowState => _$this._workflowState; + set workflowState(String? workflowState) => _$this._workflowState = workflowState; - String _submissionType; - String get submissionType => _$this._submissionType; - set submissionType(String submissionType) => + String? _submissionType; + String? get submissionType => _$this._submissionType; + set submissionType(String? submissionType) => _$this._submissionType = submissionType; - String _previewUrl; - String get previewUrl => _$this._previewUrl; - set previewUrl(String previewUrl) => _$this._previewUrl = previewUrl; + String? _previewUrl; + String? get previewUrl => _$this._previewUrl; + set previewUrl(String? previewUrl) => _$this._previewUrl = previewUrl; - String _url; - String get url => _$this._url; - set url(String url) => _$this._url = url; + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; - bool _isLate; - bool get isLate => _$this._isLate; - set isLate(bool isLate) => _$this._isLate = isLate; + bool? _isLate; + bool? get isLate => _$this._isLate; + set isLate(bool? isLate) => _$this._isLate = isLate; - bool _excused; - bool get excused => _$this._excused; - set excused(bool excused) => _$this._excused = excused; + bool? _excused; + bool? get excused => _$this._excused; + set excused(bool? excused) => _$this._excused = excused; - bool _missing; - bool get missing => _$this._missing; - set missing(bool missing) => _$this._missing = missing; + bool? _missing; + bool? get missing => _$this._missing; + set missing(bool? missing) => _$this._missing = missing; - String _assignmentId; - String get assignmentId => _$this._assignmentId; - set assignmentId(String assignmentId) => _$this._assignmentId = assignmentId; + String? _assignmentId; + String? get assignmentId => _$this._assignmentId; + set assignmentId(String? assignmentId) => _$this._assignmentId = assignmentId; - AssignmentBuilder _assignment; + AssignmentBuilder? _assignment; AssignmentBuilder get assignment => _$this._assignment ??= new AssignmentBuilder(); - set assignment(AssignmentBuilder assignment) => + set assignment(AssignmentBuilder? assignment) => _$this._assignment = assignment; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _graderId; - String get graderId => _$this._graderId; - set graderId(String graderId) => _$this._graderId = graderId; + String? _graderId; + String? get graderId => _$this._graderId; + set graderId(String? graderId) => _$this._graderId = graderId; - UserBuilder _user; + UserBuilder? _user; UserBuilder get user => _$this._user ??= new UserBuilder(); - set user(UserBuilder user) => _$this._user = user; + set user(UserBuilder? user) => _$this._user = user; - double _pointsDeducted; - double get pointsDeducted => _$this._pointsDeducted; - set pointsDeducted(double pointsDeducted) => + double? _pointsDeducted; + double? get pointsDeducted => _$this._pointsDeducted; + set pointsDeducted(double? pointsDeducted) => _$this._pointsDeducted = pointsDeducted; - double _enteredScore; - double get enteredScore => _$this._enteredScore; - set enteredScore(double enteredScore) => _$this._enteredScore = enteredScore; + double? _enteredScore; + double? get enteredScore => _$this._enteredScore; + set enteredScore(double? enteredScore) => _$this._enteredScore = enteredScore; - String _enteredGrade; - String get enteredGrade => _$this._enteredGrade; - set enteredGrade(String enteredGrade) => _$this._enteredGrade = enteredGrade; + String? _enteredGrade; + String? get enteredGrade => _$this._enteredGrade; + set enteredGrade(String? enteredGrade) => _$this._enteredGrade = enteredGrade; - DateTime _postedAt; - DateTime get postedAt => _$this._postedAt; - set postedAt(DateTime postedAt) => _$this._postedAt = postedAt; + DateTime? _postedAt; + DateTime? get postedAt => _$this._postedAt; + set postedAt(DateTime? postedAt) => _$this._postedAt = postedAt; SubmissionBuilder() { Submission._initializeBuilder(this); } SubmissionBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _grade = _$v.grade; - _score = _$v.score; - _attempt = _$v.attempt; - _submittedAt = _$v.submittedAt; - _commentCreated = _$v.commentCreated; - _mediaContentType = _$v.mediaContentType; - _mediaCommentUrl = _$v.mediaCommentUrl; - _mediaCommentDisplay = _$v.mediaCommentDisplay; - _submissionHistory = _$v.submissionHistory?.toBuilder(); - _body = _$v.body; - _isGradeMatchesCurrentSubmission = _$v.isGradeMatchesCurrentSubmission; - _workflowState = _$v.workflowState; - _submissionType = _$v.submissionType; - _previewUrl = _$v.previewUrl; - _url = _$v.url; - _isLate = _$v.isLate; - _excused = _$v.excused; - _missing = _$v.missing; - _assignmentId = _$v.assignmentId; - _assignment = _$v.assignment?.toBuilder(); - _userId = _$v.userId; - _graderId = _$v.graderId; - _user = _$v.user?.toBuilder(); - _pointsDeducted = _$v.pointsDeducted; - _enteredScore = _$v.enteredScore; - _enteredGrade = _$v.enteredGrade; - _postedAt = _$v.postedAt; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _grade = $v.grade; + _score = $v.score; + _attempt = $v.attempt; + _submittedAt = $v.submittedAt; + _commentCreated = $v.commentCreated; + _mediaContentType = $v.mediaContentType; + _mediaCommentUrl = $v.mediaCommentUrl; + _mediaCommentDisplay = $v.mediaCommentDisplay; + _submissionHistory = $v.submissionHistory.toBuilder(); + _body = $v.body; + _isGradeMatchesCurrentSubmission = $v.isGradeMatchesCurrentSubmission; + _workflowState = $v.workflowState; + _submissionType = $v.submissionType; + _previewUrl = $v.previewUrl; + _url = $v.url; + _isLate = $v.isLate; + _excused = $v.excused; + _missing = $v.missing; + _assignmentId = $v.assignmentId; + _assignment = $v.assignment?.toBuilder(); + _userId = $v.userId; + _graderId = $v.graderId; + _user = $v.user?.toBuilder(); + _pointsDeducted = $v.pointsDeducted; + _enteredScore = $v.enteredScore; + _enteredGrade = $v.enteredGrade; + _postedAt = $v.postedAt; _$v = null; } return this; @@ -720,27 +677,30 @@ class SubmissionBuilder implements Builder { @override void replace(Submission other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Submission; } @override - void update(void Function(SubmissionBuilder) updates) { + void update(void Function(SubmissionBuilder)? updates) { if (updates != null) updates(this); } @override - _$Submission build() { + Submission build() => _build(); + + _$Submission _build() { _$Submission _$result; try { _$result = _$v ?? new _$Submission._( - id: id, + id: BuiltValueNullFieldError.checkNotNull( + id, r'Submission', 'id'), grade: grade, - score: score, - attempt: attempt, + score: BuiltValueNullFieldError.checkNotNull( + score, r'Submission', 'score'), + attempt: BuiltValueNullFieldError.checkNotNull( + attempt, r'Submission', 'attempt'), submittedAt: submittedAt, commentCreated: commentCreated, mediaContentType: mediaContentType, @@ -748,25 +708,34 @@ class SubmissionBuilder implements Builder { mediaCommentDisplay: mediaCommentDisplay, submissionHistory: submissionHistory.build(), body: body, - isGradeMatchesCurrentSubmission: isGradeMatchesCurrentSubmission, + isGradeMatchesCurrentSubmission: + BuiltValueNullFieldError.checkNotNull( + isGradeMatchesCurrentSubmission, + r'Submission', + 'isGradeMatchesCurrentSubmission'), workflowState: workflowState, submissionType: submissionType, previewUrl: previewUrl, url: url, - isLate: isLate, - excused: excused, - missing: missing, - assignmentId: assignmentId, + isLate: BuiltValueNullFieldError.checkNotNull( + isLate, r'Submission', 'isLate'), + excused: BuiltValueNullFieldError.checkNotNull( + excused, r'Submission', 'excused'), + missing: BuiltValueNullFieldError.checkNotNull( + missing, r'Submission', 'missing'), + assignmentId: BuiltValueNullFieldError.checkNotNull( + assignmentId, r'Submission', 'assignmentId'), assignment: _assignment?.build(), - userId: userId, - graderId: graderId, + userId: + BuiltValueNullFieldError.checkNotNull(userId, r'Submission', 'userId'), + graderId: BuiltValueNullFieldError.checkNotNull(graderId, r'Submission', 'graderId'), user: _user?.build(), pointsDeducted: pointsDeducted, - enteredScore: enteredScore, + enteredScore: BuiltValueNullFieldError.checkNotNull(enteredScore, r'Submission', 'enteredScore'), enteredGrade: enteredGrade, postedAt: postedAt); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'submissionHistory'; submissionHistory.build(); @@ -778,7 +747,7 @@ class SubmissionBuilder implements Builder { _user?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'Submission', _$failedField, e.toString()); + r'Submission', _$failedField, e.toString()); } rethrow; } @@ -787,4 +756,4 @@ class SubmissionBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/submission_wrapper.dart b/apps/flutter_parent/lib/models/submission_wrapper.dart index 711d6e4997..363c676039 100644 --- a/apps/flutter_parent/lib/models/submission_wrapper.dart +++ b/apps/flutter_parent/lib/models/submission_wrapper.dart @@ -31,11 +31,9 @@ abstract class SubmissionWrapper implements Built get serializer => SubmissionWrapperSerializer(); - @nullable - Submission get submission; + Submission? get submission; - @nullable - BuiltList get submissionList; + BuiltList? get submissionList; SubmissionWrapper._(); @@ -52,7 +50,7 @@ class SubmissionWrapperSerializer implements StructuredSerializer.from(submissionList).toBuilder(); @@ -73,7 +71,7 @@ class SubmissionWrapperSerializer implements StructuredSerializer[]; + final result = []; // Regardless of how we were storing it, we need to serialize it as "submission" since that's what the api expects if (object.submission != null) { diff --git a/apps/flutter_parent/lib/models/submission_wrapper.g.dart b/apps/flutter_parent/lib/models/submission_wrapper.g.dart index 5400012ae9..55afdbae1e 100644 --- a/apps/flutter_parent/lib/models/submission_wrapper.g.dart +++ b/apps/flutter_parent/lib/models/submission_wrapper.g.dart @@ -8,13 +8,13 @@ part of 'submission_wrapper.dart'; class _$SubmissionWrapper extends SubmissionWrapper { @override - final Submission submission; + final Submission? submission; @override - final BuiltList submissionList; + final BuiltList? submissionList; factory _$SubmissionWrapper( - [void Function(SubmissionWrapperBuilder) updates]) => - (new SubmissionWrapperBuilder()..update(updates)).build(); + [void Function(SubmissionWrapperBuilder)? updates]) => + (new SubmissionWrapperBuilder()..update(updates))._build(); _$SubmissionWrapper._({this.submission, this.submissionList}) : super._(); @@ -36,12 +36,16 @@ class _$SubmissionWrapper extends SubmissionWrapper { @override int get hashCode { - return $jf($jc($jc(0, submission.hashCode), submissionList.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, submission.hashCode); + _$hash = $jc(_$hash, submissionList.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('SubmissionWrapper') + return (newBuiltValueToStringHelper(r'SubmissionWrapper') ..add('submission', submission) ..add('submissionList', submissionList)) .toString(); @@ -50,26 +54,27 @@ class _$SubmissionWrapper extends SubmissionWrapper { class SubmissionWrapperBuilder implements Builder { - _$SubmissionWrapper _$v; + _$SubmissionWrapper? _$v; - SubmissionBuilder _submission; + SubmissionBuilder? _submission; SubmissionBuilder get submission => _$this._submission ??= new SubmissionBuilder(); - set submission(SubmissionBuilder submission) => + set submission(SubmissionBuilder? submission) => _$this._submission = submission; - ListBuilder _submissionList; + ListBuilder? _submissionList; ListBuilder get submissionList => _$this._submissionList ??= new ListBuilder(); - set submissionList(ListBuilder submissionList) => + set submissionList(ListBuilder? submissionList) => _$this._submissionList = submissionList; SubmissionWrapperBuilder(); SubmissionWrapperBuilder get _$this { - if (_$v != null) { - _submission = _$v.submission?.toBuilder(); - _submissionList = _$v.submissionList?.toBuilder(); + final $v = _$v; + if ($v != null) { + _submission = $v.submission?.toBuilder(); + _submissionList = $v.submissionList?.toBuilder(); _$v = null; } return this; @@ -77,19 +82,19 @@ class SubmissionWrapperBuilder @override void replace(SubmissionWrapper other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$SubmissionWrapper; } @override - void update(void Function(SubmissionWrapperBuilder) updates) { + void update(void Function(SubmissionWrapperBuilder)? updates) { if (updates != null) updates(this); } @override - _$SubmissionWrapper build() { + SubmissionWrapper build() => _build(); + + _$SubmissionWrapper _build() { _$SubmissionWrapper _$result; try { _$result = _$v ?? @@ -97,7 +102,7 @@ class SubmissionWrapperBuilder submission: _submission?.build(), submissionList: _submissionList?.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'submission'; _submission?.build(); @@ -105,7 +110,7 @@ class SubmissionWrapperBuilder _submissionList?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'SubmissionWrapper', _$failedField, e.toString()); + r'SubmissionWrapper', _$failedField, e.toString()); } rethrow; } @@ -114,4 +119,4 @@ class SubmissionWrapperBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/term.dart b/apps/flutter_parent/lib/models/term.dart index 724c9f35de..46b59549f9 100644 --- a/apps/flutter_parent/lib/models/term.dart +++ b/apps/flutter_parent/lib/models/term.dart @@ -27,16 +27,13 @@ abstract class Term implements Built { String get id; - @nullable - String get name; + String? get name; @BuiltValueField(wireName: 'start_at') - @nullable - DateTime get startAt; + DateTime? get startAt; @BuiltValueField(wireName: 'end_at') - @nullable - DateTime get endAt; + DateTime? get endAt; Term._(); factory Term([void Function(TermBuilder) updates]) = _$Term; diff --git a/apps/flutter_parent/lib/models/term.g.dart b/apps/flutter_parent/lib/models/term.g.dart index a6f48e8df3..d5f121530d 100644 --- a/apps/flutter_parent/lib/models/term.g.dart +++ b/apps/flutter_parent/lib/models/term.g.dart @@ -15,63 +15,61 @@ class _$TermSerializer implements StructuredSerializer { final String wireName = 'Term'; @override - Iterable serialize(Serializers serializers, Term object, + Iterable serialize(Serializers serializers, Term object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), ]; - result.add('name'); - if (object.name == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.name, - specifiedType: const FullType(String))); - } - result.add('start_at'); - if (object.startAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.startAt, + Object? value; + value = object.name; + + result + ..add('name') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.startAt; + + result + ..add('start_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } - result.add('end_at'); - if (object.endAt == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.endAt, + value = object.endAt; + + result + ..add('end_at') + ..add(serializers.serialize(value, specifiedType: const FullType(DateTime))); - } + return result; } @override - Term deserialize(Serializers serializers, Iterable serialized, + Term deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new TermBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'start_at': result.startAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; case 'end_at': result.endAt = serializers.deserialize(value, - specifiedType: const FullType(DateTime)) as DateTime; + specifiedType: const FullType(DateTime)) as DateTime?; break; } } @@ -84,19 +82,18 @@ class _$Term extends Term { @override final String id; @override - final String name; + final String? name; @override - final DateTime startAt; + final DateTime? startAt; @override - final DateTime endAt; + final DateTime? endAt; - factory _$Term([void Function(TermBuilder) updates]) => - (new TermBuilder()..update(updates)).build(); + factory _$Term([void Function(TermBuilder)? updates]) => + (new TermBuilder()..update(updates))._build(); - _$Term._({this.id, this.name, this.startAt, this.endAt}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Term', 'id'); - } + _$Term._({required this.id, this.name, this.startAt, this.endAt}) + : super._() { + BuiltValueNullFieldError.checkNotNull(id, r'Term', 'id'); } @override @@ -118,14 +115,18 @@ class _$Term extends Term { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, id.hashCode), name.hashCode), startAt.hashCode), - endAt.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, startAt.hashCode); + _$hash = $jc(_$hash, endAt.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('Term') + return (newBuiltValueToStringHelper(r'Term') ..add('id', id) ..add('name', name) ..add('startAt', startAt) @@ -135,34 +136,35 @@ class _$Term extends Term { } class TermBuilder implements Builder { - _$Term _$v; + _$Term? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - DateTime _startAt; - DateTime get startAt => _$this._startAt; - set startAt(DateTime startAt) => _$this._startAt = startAt; + DateTime? _startAt; + DateTime? get startAt => _$this._startAt; + set startAt(DateTime? startAt) => _$this._startAt = startAt; - DateTime _endAt; - DateTime get endAt => _$this._endAt; - set endAt(DateTime endAt) => _$this._endAt = endAt; + DateTime? _endAt; + DateTime? get endAt => _$this._endAt; + set endAt(DateTime? endAt) => _$this._endAt = endAt; TermBuilder() { Term._initializeBuilder(this); } TermBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; - _startAt = _$v.startAt; - _endAt = _$v.endAt; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _startAt = $v.startAt; + _endAt = $v.endAt; _$v = null; } return this; @@ -170,24 +172,28 @@ class TermBuilder implements Builder { @override void replace(Term other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Term; } @override - void update(void Function(TermBuilder) updates) { + void update(void Function(TermBuilder)? updates) { if (updates != null) updates(this); } @override - _$Term build() { - final _$result = - _$v ?? new _$Term._(id: id, name: name, startAt: startAt, endAt: endAt); + Term build() => _build(); + + _$Term _build() { + final _$result = _$v ?? + new _$Term._( + id: BuiltValueNullFieldError.checkNotNull(id, r'Term', 'id'), + name: name, + startAt: startAt, + endAt: endAt); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/terms_of_service.dart b/apps/flutter_parent/lib/models/terms_of_service.dart index ae1ba4001f..5e4d11ed52 100644 --- a/apps/flutter_parent/lib/models/terms_of_service.dart +++ b/apps/flutter_parent/lib/models/terms_of_service.dart @@ -26,16 +26,14 @@ abstract class TermsOfService implements Built serialize(Serializers serializers, TermsOfService object, + Iterable serialize(Serializers serializers, TermsOfService object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'passive', @@ -29,55 +29,54 @@ class _$TermsOfServiceSerializer serializers.serialize(object.accountId, specifiedType: const FullType(String)), ]; - result.add('terms_type'); - if (object.termsType == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.termsType, - specifiedType: const FullType(String))); - } - result.add('content'); - if (object.content == null) { - result.add(null); - } else { - result.add(serializers.serialize(object.content, - specifiedType: const FullType(String))); - } + Object? value; + value = object.termsType; + + result + ..add('terms_type') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + value = object.content; + + result + ..add('content') + ..add( + serializers.serialize(value, specifiedType: const FullType(String))); + return result; } @override TermsOfService deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new TermsOfServiceBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'terms_type': result.termsType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'passive': result.passive = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'account_id': result.accountId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'content': result.content = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -90,29 +89,29 @@ class _$TermsOfService extends TermsOfService { @override final String id; @override - final String termsType; + final String? termsType; @override final bool passive; @override final String accountId; @override - final String content; + final String? content; - factory _$TermsOfService([void Function(TermsOfServiceBuilder) updates]) => - (new TermsOfServiceBuilder()..update(updates)).build(); + factory _$TermsOfService([void Function(TermsOfServiceBuilder)? updates]) => + (new TermsOfServiceBuilder()..update(updates))._build(); _$TermsOfService._( - {this.id, this.termsType, this.passive, this.accountId, this.content}) + {required this.id, + this.termsType, + required this.passive, + required this.accountId, + this.content}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('TermsOfService', 'id'); - } - if (passive == null) { - throw new BuiltValueNullFieldError('TermsOfService', 'passive'); - } - if (accountId == null) { - throw new BuiltValueNullFieldError('TermsOfService', 'accountId'); - } + BuiltValueNullFieldError.checkNotNull(id, r'TermsOfService', 'id'); + BuiltValueNullFieldError.checkNotNull( + passive, r'TermsOfService', 'passive'); + BuiltValueNullFieldError.checkNotNull( + accountId, r'TermsOfService', 'accountId'); } @override @@ -136,15 +135,19 @@ class _$TermsOfService extends TermsOfService { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), termsType.hashCode), passive.hashCode), - accountId.hashCode), - content.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, termsType.hashCode); + _$hash = $jc(_$hash, passive.hashCode); + _$hash = $jc(_$hash, accountId.hashCode); + _$hash = $jc(_$hash, content.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('TermsOfService') + return (newBuiltValueToStringHelper(r'TermsOfService') ..add('id', id) ..add('termsType', termsType) ..add('passive', passive) @@ -156,37 +159,38 @@ class _$TermsOfService extends TermsOfService { class TermsOfServiceBuilder implements Builder { - _$TermsOfService _$v; + _$TermsOfService? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _termsType; - String get termsType => _$this._termsType; - set termsType(String termsType) => _$this._termsType = termsType; + String? _termsType; + String? get termsType => _$this._termsType; + set termsType(String? termsType) => _$this._termsType = termsType; - bool _passive; - bool get passive => _$this._passive; - set passive(bool passive) => _$this._passive = passive; + bool? _passive; + bool? get passive => _$this._passive; + set passive(bool? passive) => _$this._passive = passive; - String _accountId; - String get accountId => _$this._accountId; - set accountId(String accountId) => _$this._accountId = accountId; + String? _accountId; + String? get accountId => _$this._accountId; + set accountId(String? accountId) => _$this._accountId = accountId; - String _content; - String get content => _$this._content; - set content(String content) => _$this._content = content; + String? _content; + String? get content => _$this._content; + set content(String? content) => _$this._content = content; TermsOfServiceBuilder(); TermsOfServiceBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _termsType = _$v.termsType; - _passive = _$v.passive; - _accountId = _$v.accountId; - _content = _$v.content; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _termsType = $v.termsType; + _passive = $v.passive; + _accountId = $v.accountId; + _content = $v.content; _$v = null; } return this; @@ -194,29 +198,32 @@ class TermsOfServiceBuilder @override void replace(TermsOfService other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$TermsOfService; } @override - void update(void Function(TermsOfServiceBuilder) updates) { + void update(void Function(TermsOfServiceBuilder)? updates) { if (updates != null) updates(this); } @override - _$TermsOfService build() { + TermsOfService build() => _build(); + + _$TermsOfService _build() { final _$result = _$v ?? new _$TermsOfService._( - id: id, + id: BuiltValueNullFieldError.checkNotNull( + id, r'TermsOfService', 'id'), termsType: termsType, - passive: passive, - accountId: accountId, + passive: BuiltValueNullFieldError.checkNotNull( + passive, r'TermsOfService', 'passive'), + accountId: BuiltValueNullFieldError.checkNotNull( + accountId, r'TermsOfService', 'accountId'), content: content); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/unread_count.g.dart b/apps/flutter_parent/lib/models/unread_count.g.dart index e3958866ce..4de0ec9265 100644 --- a/apps/flutter_parent/lib/models/unread_count.g.dart +++ b/apps/flutter_parent/lib/models/unread_count.g.dart @@ -15,9 +15,9 @@ class _$UnreadCountSerializer implements StructuredSerializer { final String wireName = 'UnreadCount'; @override - Iterable serialize(Serializers serializers, UnreadCount object, + Iterable serialize(Serializers serializers, UnreadCount object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'unread_count', serializers.serialize(object.count, specifiedType: const FullType(JsonObject)), @@ -27,20 +27,19 @@ class _$UnreadCountSerializer implements StructuredSerializer { } @override - UnreadCount deserialize(Serializers serializers, Iterable serialized, + UnreadCount deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new UnreadCountBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; - if (value == null) continue; + final Object? value = iterator.current; switch (key) { case 'unread_count': result.count = serializers.deserialize(value, - specifiedType: const FullType(JsonObject)) as JsonObject; + specifiedType: const FullType(JsonObject))! as JsonObject; break; } } @@ -53,13 +52,11 @@ class _$UnreadCount extends UnreadCount { @override final JsonObject count; - factory _$UnreadCount([void Function(UnreadCountBuilder) updates]) => - (new UnreadCountBuilder()..update(updates)).build(); + factory _$UnreadCount([void Function(UnreadCountBuilder)? updates]) => + (new UnreadCountBuilder()..update(updates))._build(); - _$UnreadCount._({this.count}) : super._() { - if (count == null) { - throw new BuiltValueNullFieldError('UnreadCount', 'count'); - } + _$UnreadCount._({required this.count}) : super._() { + BuiltValueNullFieldError.checkNotNull(count, r'UnreadCount', 'count'); } @override @@ -77,28 +74,32 @@ class _$UnreadCount extends UnreadCount { @override int get hashCode { - return $jf($jc(0, count.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, count.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('UnreadCount')..add('count', count)) + return (newBuiltValueToStringHelper(r'UnreadCount')..add('count', count)) .toString(); } } class UnreadCountBuilder implements Builder { - _$UnreadCount _$v; + _$UnreadCount? _$v; - JsonObject _count; - JsonObject get count => _$this._count; - set count(JsonObject count) => _$this._count = count; + JsonObject? _count; + JsonObject? get count => _$this._count; + set count(JsonObject? count) => _$this._count = count; UnreadCountBuilder(); UnreadCountBuilder get _$this { - if (_$v != null) { - _count = _$v.count; + final $v = _$v; + if ($v != null) { + _count = $v.count; _$v = null; } return this; @@ -106,23 +107,26 @@ class UnreadCountBuilder implements Builder { @override void replace(UnreadCount other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$UnreadCount; } @override - void update(void Function(UnreadCountBuilder) updates) { + void update(void Function(UnreadCountBuilder)? updates) { if (updates != null) updates(this); } @override - _$UnreadCount build() { - final _$result = _$v ?? new _$UnreadCount._(count: count); + UnreadCount build() => _build(); + + _$UnreadCount _build() { + final _$result = _$v ?? + new _$UnreadCount._( + count: BuiltValueNullFieldError.checkNotNull( + count, r'UnreadCount', 'count')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/user.dart b/apps/flutter_parent/lib/models/user.dart index d55ab108f1..53626e996a 100644 --- a/apps/flutter_parent/lib/models/user.dart +++ b/apps/flutter_parent/lib/models/user.dart @@ -31,38 +31,29 @@ abstract class User implements Built { String get name; - @nullable @BuiltValueField(wireName: 'sortable_name') - String get sortableName; + String? get sortableName; - @nullable @BuiltValueField(wireName: 'short_name') - String get shortName; + String? get shortName; - @nullable - String get pronouns; + String? get pronouns; - @nullable @BuiltValueField(wireName: 'avatar_url') - String get avatarUrl; + String? get avatarUrl; - @nullable @BuiltValueField(wireName: 'primary_email') - String get primaryEmail; + String? get primaryEmail; - @nullable - String get locale; + String? get locale; - @nullable @BuiltValueField(wireName: 'effective_locale') - String get effectiveLocale; + String? get effectiveLocale; - @nullable - UserPermission get permissions; + UserPermission? get permissions; - @nullable @BuiltValueField(wireName: 'login_id') - String get loginId; + String? get loginId; static void _initializeBuilder(UserBuilder b) => b ..id = '' diff --git a/apps/flutter_parent/lib/models/user.g.dart b/apps/flutter_parent/lib/models/user.g.dart index 258aefd8f8..827499fda0 100644 --- a/apps/flutter_parent/lib/models/user.g.dart +++ b/apps/flutter_parent/lib/models/user.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of user; +part of 'user.dart'; // ************************************************************************** // BuiltValueGenerator @@ -17,15 +17,15 @@ class _$UserSerializer implements StructuredSerializer { final String wireName = 'User'; @override - Iterable serialize(Serializers serializers, User object, + Iterable serialize(Serializers serializers, User object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), ]; - Object value; + Object? value; value = object.sortableName; result @@ -85,59 +85,60 @@ class _$UserSerializer implements StructuredSerializer { } @override - User deserialize(Serializers serializers, Iterable serialized, + User deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new UserBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'sortable_name': result.sortableName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'short_name': result.shortName = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'pronouns': result.pronouns = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'avatar_url': result.avatarUrl = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'primary_email': result.primaryEmail = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'locale': result.locale = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'effective_locale': result.effectiveLocale = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; case 'permissions': result.permissions.replace(serializers.deserialize(value, - specifiedType: const FullType(UserPermission)) as UserPermission); + specifiedType: const FullType(UserPermission))! + as UserPermission); break; case 'login_id': result.loginId = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String)) as String?; break; } } @@ -154,9 +155,9 @@ class _$UserPermissionSerializer final String wireName = 'UserPermission'; @override - Iterable serialize(Serializers serializers, UserPermission object, + Iterable serialize(Serializers serializers, UserPermission object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'become_user', serializers.serialize(object.become_user, specifiedType: const FullType(bool)), @@ -176,31 +177,31 @@ class _$UserPermissionSerializer @override UserPermission deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new UserPermissionBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final Object value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'become_user': result.become_user = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'can_update_name': result.canUpdateName = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'can_update_avatar': result.canUpdateAvatar = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; case 'limit_parent_app_web_access': result.limitParentAppWebAccess = serializers.deserialize(value, - specifiedType: const FullType(bool)) as bool; + specifiedType: const FullType(bool))! as bool; break; } } @@ -215,30 +216,30 @@ class _$User extends User { @override final String name; @override - final String sortableName; + final String? sortableName; @override - final String shortName; + final String? shortName; @override - final String pronouns; + final String? pronouns; @override - final String avatarUrl; + final String? avatarUrl; @override - final String primaryEmail; + final String? primaryEmail; @override - final String locale; + final String? locale; @override - final String effectiveLocale; + final String? effectiveLocale; @override - final UserPermission permissions; + final UserPermission? permissions; @override - final String loginId; + final String? loginId; - factory _$User([void Function(UserBuilder) updates]) => - (new UserBuilder()..update(updates)).build(); + factory _$User([void Function(UserBuilder)? updates]) => + (new UserBuilder()..update(updates))._build(); _$User._( - {this.id, - this.name, + {required this.id, + required this.name, this.sortableName, this.shortName, this.pronouns, @@ -249,8 +250,8 @@ class _$User extends User { this.permissions, this.loginId}) : super._() { - BuiltValueNullFieldError.checkNotNull(id, 'User', 'id'); - BuiltValueNullFieldError.checkNotNull(name, 'User', 'name'); + BuiltValueNullFieldError.checkNotNull(id, r'User', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'User', 'name'); } @override @@ -279,29 +280,25 @@ class _$User extends User { @override int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc($jc($jc(0, id.hashCode), name.hashCode), - sortableName.hashCode), - shortName.hashCode), - pronouns.hashCode), - avatarUrl.hashCode), - primaryEmail.hashCode), - locale.hashCode), - effectiveLocale.hashCode), - permissions.hashCode), - loginId.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, sortableName.hashCode); + _$hash = $jc(_$hash, shortName.hashCode); + _$hash = $jc(_$hash, pronouns.hashCode); + _$hash = $jc(_$hash, avatarUrl.hashCode); + _$hash = $jc(_$hash, primaryEmail.hashCode); + _$hash = $jc(_$hash, locale.hashCode); + _$hash = $jc(_$hash, effectiveLocale.hashCode); + _$hash = $jc(_$hash, permissions.hashCode); + _$hash = $jc(_$hash, loginId.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('User') + return (newBuiltValueToStringHelper(r'User') ..add('id', id) ..add('name', name) ..add('sortableName', sortableName) @@ -318,54 +315,54 @@ class _$User extends User { } class UserBuilder implements Builder { - _$User _$v; + _$User? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; - String _sortableName; - String get sortableName => _$this._sortableName; - set sortableName(String sortableName) => _$this._sortableName = sortableName; + String? _sortableName; + String? get sortableName => _$this._sortableName; + set sortableName(String? sortableName) => _$this._sortableName = sortableName; - String _shortName; - String get shortName => _$this._shortName; - set shortName(String shortName) => _$this._shortName = shortName; + String? _shortName; + String? get shortName => _$this._shortName; + set shortName(String? shortName) => _$this._shortName = shortName; - String _pronouns; - String get pronouns => _$this._pronouns; - set pronouns(String pronouns) => _$this._pronouns = pronouns; + String? _pronouns; + String? get pronouns => _$this._pronouns; + set pronouns(String? pronouns) => _$this._pronouns = pronouns; - String _avatarUrl; - String get avatarUrl => _$this._avatarUrl; - set avatarUrl(String avatarUrl) => _$this._avatarUrl = avatarUrl; + String? _avatarUrl; + String? get avatarUrl => _$this._avatarUrl; + set avatarUrl(String? avatarUrl) => _$this._avatarUrl = avatarUrl; - String _primaryEmail; - String get primaryEmail => _$this._primaryEmail; - set primaryEmail(String primaryEmail) => _$this._primaryEmail = primaryEmail; + String? _primaryEmail; + String? get primaryEmail => _$this._primaryEmail; + set primaryEmail(String? primaryEmail) => _$this._primaryEmail = primaryEmail; - String _locale; - String get locale => _$this._locale; - set locale(String locale) => _$this._locale = locale; + String? _locale; + String? get locale => _$this._locale; + set locale(String? locale) => _$this._locale = locale; - String _effectiveLocale; - String get effectiveLocale => _$this._effectiveLocale; - set effectiveLocale(String effectiveLocale) => + String? _effectiveLocale; + String? get effectiveLocale => _$this._effectiveLocale; + set effectiveLocale(String? effectiveLocale) => _$this._effectiveLocale = effectiveLocale; - UserPermissionBuilder _permissions; + UserPermissionBuilder? _permissions; UserPermissionBuilder get permissions => _$this._permissions ??= new UserPermissionBuilder(); - set permissions(UserPermissionBuilder permissions) => + set permissions(UserPermissionBuilder? permissions) => _$this._permissions = permissions; - String _loginId; - String get loginId => _$this._loginId; - set loginId(String loginId) => _$this._loginId = loginId; + String? _loginId; + String? get loginId => _$this._loginId; + set loginId(String? loginId) => _$this._loginId = loginId; UserBuilder() { User._initializeBuilder(this); @@ -397,18 +394,21 @@ class UserBuilder implements Builder { } @override - void update(void Function(UserBuilder) updates) { + void update(void Function(UserBuilder)? updates) { if (updates != null) updates(this); } @override - _$User build() { + User build() => _build(); + + _$User _build() { _$User _$result; try { _$result = _$v ?? new _$User._( - id: BuiltValueNullFieldError.checkNotNull(id, 'User', 'id'), - name: BuiltValueNullFieldError.checkNotNull(name, 'User', 'name'), + id: BuiltValueNullFieldError.checkNotNull(id, r'User', 'id'), + name: + BuiltValueNullFieldError.checkNotNull(name, r'User', 'name'), sortableName: sortableName, shortName: shortName, pronouns: pronouns, @@ -419,13 +419,13 @@ class UserBuilder implements Builder { permissions: _permissions?.build(), loginId: loginId); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'permissions'; _permissions?.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'User', _$failedField, e.toString()); + r'User', _$failedField, e.toString()); } rethrow; } @@ -444,23 +444,23 @@ class _$UserPermission extends UserPermission { @override final bool limitParentAppWebAccess; - factory _$UserPermission([void Function(UserPermissionBuilder) updates]) => - (new UserPermissionBuilder()..update(updates)).build(); + factory _$UserPermission([void Function(UserPermissionBuilder)? updates]) => + (new UserPermissionBuilder()..update(updates))._build(); _$UserPermission._( - {this.become_user, - this.canUpdateName, - this.canUpdateAvatar, - this.limitParentAppWebAccess}) + {required this.become_user, + required this.canUpdateName, + required this.canUpdateAvatar, + required this.limitParentAppWebAccess}) : super._() { BuiltValueNullFieldError.checkNotNull( - become_user, 'UserPermission', 'become_user'); + become_user, r'UserPermission', 'become_user'); BuiltValueNullFieldError.checkNotNull( - canUpdateName, 'UserPermission', 'canUpdateName'); + canUpdateName, r'UserPermission', 'canUpdateName'); BuiltValueNullFieldError.checkNotNull( - canUpdateAvatar, 'UserPermission', 'canUpdateAvatar'); + canUpdateAvatar, r'UserPermission', 'canUpdateAvatar'); BuiltValueNullFieldError.checkNotNull( - limitParentAppWebAccess, 'UserPermission', 'limitParentAppWebAccess'); + limitParentAppWebAccess, r'UserPermission', 'limitParentAppWebAccess'); } @override @@ -483,15 +483,18 @@ class _$UserPermission extends UserPermission { @override int get hashCode { - return $jf($jc( - $jc($jc($jc(0, become_user.hashCode), canUpdateName.hashCode), - canUpdateAvatar.hashCode), - limitParentAppWebAccess.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, become_user.hashCode); + _$hash = $jc(_$hash, canUpdateName.hashCode); + _$hash = $jc(_$hash, canUpdateAvatar.hashCode); + _$hash = $jc(_$hash, limitParentAppWebAccess.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('UserPermission') + return (newBuiltValueToStringHelper(r'UserPermission') ..add('become_user', become_user) ..add('canUpdateName', canUpdateName) ..add('canUpdateAvatar', canUpdateAvatar) @@ -502,25 +505,25 @@ class _$UserPermission extends UserPermission { class UserPermissionBuilder implements Builder { - _$UserPermission _$v; + _$UserPermission? _$v; - bool _become_user; - bool get become_user => _$this._become_user; - set become_user(bool become_user) => _$this._become_user = become_user; + bool? _become_user; + bool? get become_user => _$this._become_user; + set become_user(bool? become_user) => _$this._become_user = become_user; - bool _canUpdateName; - bool get canUpdateName => _$this._canUpdateName; - set canUpdateName(bool canUpdateName) => + bool? _canUpdateName; + bool? get canUpdateName => _$this._canUpdateName; + set canUpdateName(bool? canUpdateName) => _$this._canUpdateName = canUpdateName; - bool _canUpdateAvatar; - bool get canUpdateAvatar => _$this._canUpdateAvatar; - set canUpdateAvatar(bool canUpdateAvatar) => + bool? _canUpdateAvatar; + bool? get canUpdateAvatar => _$this._canUpdateAvatar; + set canUpdateAvatar(bool? canUpdateAvatar) => _$this._canUpdateAvatar = canUpdateAvatar; - bool _limitParentAppWebAccess; - bool get limitParentAppWebAccess => _$this._limitParentAppWebAccess; - set limitParentAppWebAccess(bool limitParentAppWebAccess) => + bool? _limitParentAppWebAccess; + bool? get limitParentAppWebAccess => _$this._limitParentAppWebAccess; + set limitParentAppWebAccess(bool? limitParentAppWebAccess) => _$this._limitParentAppWebAccess = limitParentAppWebAccess; UserPermissionBuilder() { @@ -546,27 +549,29 @@ class UserPermissionBuilder } @override - void update(void Function(UserPermissionBuilder) updates) { + void update(void Function(UserPermissionBuilder)? updates) { if (updates != null) updates(this); } @override - _$UserPermission build() { + UserPermission build() => _build(); + + _$UserPermission _build() { final _$result = _$v ?? new _$UserPermission._( become_user: BuiltValueNullFieldError.checkNotNull( - become_user, 'UserPermission', 'become_user'), + become_user, r'UserPermission', 'become_user'), canUpdateName: BuiltValueNullFieldError.checkNotNull( - canUpdateName, 'UserPermission', 'canUpdateName'), + canUpdateName, r'UserPermission', 'canUpdateName'), canUpdateAvatar: BuiltValueNullFieldError.checkNotNull( - canUpdateAvatar, 'UserPermission', 'canUpdateAvatar'), + canUpdateAvatar, r'UserPermission', 'canUpdateAvatar'), limitParentAppWebAccess: BuiltValueNullFieldError.checkNotNull( limitParentAppWebAccess, - 'UserPermission', + r'UserPermission', 'limitParentAppWebAccess')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/user_color.dart b/apps/flutter_parent/lib/models/user_color.dart index e15039795d..0ae4a75b3b 100644 --- a/apps/flutter_parent/lib/models/user_color.dart +++ b/apps/flutter_parent/lib/models/user_color.dart @@ -22,8 +22,7 @@ part 'user_color.g.dart'; /// To have this built_value be generated, run this command from the project root: /// flutter pub run build_runner build --delete-conflicting-outputs abstract class UserColor implements Built { - @nullable - int get id; + int? get id; String get userDomain; diff --git a/apps/flutter_parent/lib/models/user_color.g.dart b/apps/flutter_parent/lib/models/user_color.g.dart index 00520f0486..92d496e17f 100644 --- a/apps/flutter_parent/lib/models/user_color.g.dart +++ b/apps/flutter_parent/lib/models/user_color.g.dart @@ -8,7 +8,7 @@ part of 'user_color.dart'; class _$UserColor extends UserColor { @override - final int id; + final int? id; @override final String userDomain; @override @@ -18,24 +18,22 @@ class _$UserColor extends UserColor { @override final Color color; - factory _$UserColor([void Function(UserColorBuilder) updates]) => - (new UserColorBuilder()..update(updates)).build(); + factory _$UserColor([void Function(UserColorBuilder)? updates]) => + (new UserColorBuilder()..update(updates))._build(); _$UserColor._( - {this.id, this.userDomain, this.userId, this.canvasContext, this.color}) + {this.id, + required this.userDomain, + required this.userId, + required this.canvasContext, + required this.color}) : super._() { - if (userDomain == null) { - throw new BuiltValueNullFieldError('UserColor', 'userDomain'); - } - if (userId == null) { - throw new BuiltValueNullFieldError('UserColor', 'userId'); - } - if (canvasContext == null) { - throw new BuiltValueNullFieldError('UserColor', 'canvasContext'); - } - if (color == null) { - throw new BuiltValueNullFieldError('UserColor', 'color'); - } + BuiltValueNullFieldError.checkNotNull( + userDomain, r'UserColor', 'userDomain'); + BuiltValueNullFieldError.checkNotNull(userId, r'UserColor', 'userId'); + BuiltValueNullFieldError.checkNotNull( + canvasContext, r'UserColor', 'canvasContext'); + BuiltValueNullFieldError.checkNotNull(color, r'UserColor', 'color'); } @override @@ -58,15 +56,19 @@ class _$UserColor extends UserColor { @override int get hashCode { - return $jf($jc( - $jc($jc($jc($jc(0, id.hashCode), userDomain.hashCode), userId.hashCode), - canvasContext.hashCode), - color.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, userDomain.hashCode); + _$hash = $jc(_$hash, userId.hashCode); + _$hash = $jc(_$hash, canvasContext.hashCode); + _$hash = $jc(_$hash, color.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('UserColor') + return (newBuiltValueToStringHelper(r'UserColor') ..add('id', id) ..add('userDomain', userDomain) ..add('userId', userId) @@ -77,40 +79,41 @@ class _$UserColor extends UserColor { } class UserColorBuilder implements Builder { - _$UserColor _$v; + _$UserColor? _$v; - int _id; - int get id => _$this._id; - set id(int id) => _$this._id = id; + int? _id; + int? get id => _$this._id; + set id(int? id) => _$this._id = id; - String _userDomain; - String get userDomain => _$this._userDomain; - set userDomain(String userDomain) => _$this._userDomain = userDomain; + String? _userDomain; + String? get userDomain => _$this._userDomain; + set userDomain(String? userDomain) => _$this._userDomain = userDomain; - String _userId; - String get userId => _$this._userId; - set userId(String userId) => _$this._userId = userId; + String? _userId; + String? get userId => _$this._userId; + set userId(String? userId) => _$this._userId = userId; - String _canvasContext; - String get canvasContext => _$this._canvasContext; - set canvasContext(String canvasContext) => + String? _canvasContext; + String? get canvasContext => _$this._canvasContext; + set canvasContext(String? canvasContext) => _$this._canvasContext = canvasContext; - Color _color; - Color get color => _$this._color; - set color(Color color) => _$this._color = color; + Color? _color; + Color? get color => _$this._color; + set color(Color? color) => _$this._color = color; UserColorBuilder() { UserColor._initializeBuilder(this); } UserColorBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _userDomain = _$v.userDomain; - _userId = _$v.userId; - _canvasContext = _$v.canvasContext; - _color = _$v.color; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _userDomain = $v.userDomain; + _userId = $v.userId; + _canvasContext = $v.canvasContext; + _color = $v.color; _$v = null; } return this; @@ -118,29 +121,33 @@ class UserColorBuilder implements Builder { @override void replace(UserColor other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$UserColor; } @override - void update(void Function(UserColorBuilder) updates) { + void update(void Function(UserColorBuilder)? updates) { if (updates != null) updates(this); } @override - _$UserColor build() { + UserColor build() => _build(); + + _$UserColor _build() { final _$result = _$v ?? new _$UserColor._( id: id, - userDomain: userDomain, - userId: userId, - canvasContext: canvasContext, - color: color); + userDomain: BuiltValueNullFieldError.checkNotNull( + userDomain, r'UserColor', 'userDomain'), + userId: BuiltValueNullFieldError.checkNotNull( + userId, r'UserColor', 'userId'), + canvasContext: BuiltValueNullFieldError.checkNotNull( + canvasContext, r'UserColor', 'canvasContext'), + color: BuiltValueNullFieldError.checkNotNull( + color, r'UserColor', 'color')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/models/user_colors.g.dart b/apps/flutter_parent/lib/models/user_colors.g.dart index fc578e4537..289883dcfc 100644 --- a/apps/flutter_parent/lib/models/user_colors.g.dart +++ b/apps/flutter_parent/lib/models/user_colors.g.dart @@ -15,9 +15,9 @@ class _$UserColorsSerializer implements StructuredSerializer { final String wireName = 'UserColors'; @override - Iterable serialize(Serializers serializers, UserColors object, + Iterable serialize(Serializers serializers, UserColors object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'custom_colors', serializers.serialize(object.customColors, specifiedType: const FullType(BuiltMap, @@ -28,20 +28,20 @@ class _$UserColorsSerializer implements StructuredSerializer { } @override - UserColors deserialize(Serializers serializers, Iterable serialized, + UserColors deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new UserColorsBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'custom_colors': result.customColors.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltMap, - const [const FullType(String), const FullType(String)]))); + const [const FullType(String), const FullType(String)]))!); break; } } @@ -54,13 +54,12 @@ class _$UserColors extends UserColors { @override final BuiltMap customColors; - factory _$UserColors([void Function(UserColorsBuilder) updates]) => - (new UserColorsBuilder()..update(updates)).build(); + factory _$UserColors([void Function(UserColorsBuilder)? updates]) => + (new UserColorsBuilder()..update(updates))._build(); - _$UserColors._({this.customColors}) : super._() { - if (customColors == null) { - throw new BuiltValueNullFieldError('UserColors', 'customColors'); - } + _$UserColors._({required this.customColors}) : super._() { + BuiltValueNullFieldError.checkNotNull( + customColors, r'UserColors', 'customColors'); } @override @@ -78,24 +77,27 @@ class _$UserColors extends UserColors { @override int get hashCode { - return $jf($jc(0, customColors.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, customColors.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override String toString() { - return (newBuiltValueToStringHelper('UserColors') + return (newBuiltValueToStringHelper(r'UserColors') ..add('customColors', customColors)) .toString(); } } class UserColorsBuilder implements Builder { - _$UserColors _$v; + _$UserColors? _$v; - MapBuilder _customColors; + MapBuilder? _customColors; MapBuilder get customColors => _$this._customColors ??= new MapBuilder(); - set customColors(MapBuilder customColors) => + set customColors(MapBuilder? customColors) => _$this._customColors = customColors; UserColorsBuilder() { @@ -103,8 +105,9 @@ class UserColorsBuilder implements Builder { } UserColorsBuilder get _$this { - if (_$v != null) { - _customColors = _$v.customColors?.toBuilder(); + final $v = _$v; + if ($v != null) { + _customColors = $v.customColors.toBuilder(); _$v = null; } return this; @@ -112,30 +115,30 @@ class UserColorsBuilder implements Builder { @override void replace(UserColors other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$UserColors; } @override - void update(void Function(UserColorsBuilder) updates) { + void update(void Function(UserColorsBuilder)? updates) { if (updates != null) updates(this); } @override - _$UserColors build() { + UserColors build() => _build(); + + _$UserColors _build() { _$UserColors _$result; try { _$result = _$v ?? new _$UserColors._(customColors: customColors.build()); } catch (_) { - String _$failedField; + late String _$failedField; try { _$failedField = 'customColors'; customColors.build(); } catch (e) { throw new BuiltValueNestedFieldError( - 'UserColors', _$failedField, e.toString()); + r'UserColors', _$failedField, e.toString()); } rethrow; } @@ -144,4 +147,4 @@ class UserColorsBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/apps/flutter_parent/lib/network/api/accounts_api.dart b/apps/flutter_parent/lib/network/api/accounts_api.dart index 1abe25da79..ec93faca64 100644 --- a/apps/flutter_parent/lib/network/api/accounts_api.dart +++ b/apps/flutter_parent/lib/network/api/accounts_api.dart @@ -27,22 +27,24 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class AccountsApi { - Future> searchDomains(String query) async { + Future?> searchDomains(String query) async { var dio = DioConfig.core(cacheMaxAge: Duration(minutes: 5)).dio; return fetchList(dio.get('accounts/search', queryParameters: {'search_term': query})); } - Future getTermsOfService() { - return fetch(canvasDio().get('accounts/self/terms_of_service')); + Future getTermsOfService() async { + var dio = canvasDio(); + return fetch(dio.get('accounts/self/terms_of_service')); } - Future getTermsOfServiceForAccount(String accountId, String domain) { + Future getTermsOfServiceForAccount(String accountId, String domain) async { var dio = DioConfig(baseUrl: 'https://$domain/api/v1/', forceRefresh: true).dio; return fetch(dio.get('accounts/$accountId/terms_of_service')); } - Future getAccountPermissions() { - return fetch(canvasDio().get('accounts/self/permissions')); + Future getAccountPermissions() async { + var dio = canvasDio(); + return fetch(dio.get('accounts/self/permissions')); } /** @@ -51,7 +53,8 @@ class AccountsApi { * Awaiting api changes to make this call w/o authentication prior to user account creation */ Future getPairingAllowed() async { - var response = await canvasDio().get('accounts/self/authentication_providers/canvas'); + var dio = canvasDio(); + var response = await dio.get('accounts/self/authentication_providers/canvas'); var selfRegistration = response.data['self_registration']; return selfRegistration == 'observer' || selfRegistration == 'all'; } diff --git a/apps/flutter_parent/lib/network/api/alert_api.dart b/apps/flutter_parent/lib/network/api/alert_api.dart index 51777d5904..9f9355da48 100644 --- a/apps/flutter_parent/lib/network/api/alert_api.dart +++ b/apps/flutter_parent/lib/network/api/alert_api.dart @@ -22,38 +22,43 @@ const String _alertThresholdsEndpoint = 'users/self/observer_alert_thresholds'; class AlertsApi { /// Alerts were depaginated in the original parent app, then sorted by date. Depaginating here to follow suite. - Future> getAlertsDepaginated(String studentId, bool forceRefresh) async { + Future?> getAlertsDepaginated(String studentId, bool forceRefresh) async { var dio = canvasDio(forceRefresh: forceRefresh); return fetchList(dio.get('users/self/observer_alerts/$studentId'), depaginateWith: dio); } - Future updateAlertWorkflow(String studentId, String alertId, String workflowState) async { + Future updateAlertWorkflow(String studentId, String alertId, String workflowState) async { final config = DioConfig.canvas(); // Read/dismissed data has changed and makes the cache stale config.clearCache(path: 'users/self/observer_alerts/$studentId'); - return fetch(config.dio.put('users/self/observer_alerts/$alertId/$workflowState')); + var dio = config.dio; + return fetch(dio.put('users/self/observer_alerts/$alertId/$workflowState')); } // Always force a refresh when retrieving this data - Future getUnreadCount(String studentId) => fetch(canvasDio(forceRefresh: true) - .get('users/self/observer_alerts/unread_count', queryParameters: {'student_id': studentId})); - - Future> getAlertThresholds(String studentId, bool forceRefresh) async { - return fetchList(canvasDio(forceRefresh: forceRefresh) - .get(_alertThresholdsEndpoint, queryParameters: {'student_id': studentId})); + Future getUnreadCount(String studentId) async { + var dio = canvasDio(forceRefresh: true); + return fetch(dio.get('users/self/observer_alerts/unread_count', queryParameters: {'student_id': studentId})); } - Future deleteAlert(AlertThreshold threshold) async { - return fetch(canvasDio().delete('$_alertThresholdsEndpoint/${threshold.id}')); - } + Future?> getAlertThresholds(String studentId, bool forceRefresh) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetchList(dio.get(_alertThresholdsEndpoint, queryParameters: {'student_id': studentId})); + } - Future createThreshold(AlertType type, String studentId, {String value}) { - return fetch(canvasDio().post(_alertThresholdsEndpoint, data: { - 'observer_alert_threshold': { - 'alert_type': type.toApiString(), - 'user_id': studentId, - if (value != null) 'threshold': value, - } - })); - } + Future deleteAlert(AlertThreshold threshold) async { + var dio = canvasDio(); + return fetch(dio.delete('$_alertThresholdsEndpoint/${threshold.id}')); + } + + Future createThreshold(AlertType type, String studentId, {String? value}) async { + var dio = canvasDio(); + return fetch(dio.post(_alertThresholdsEndpoint, data: { + 'observer_alert_threshold': { + 'alert_type': type.toApiString(), + 'user_id': studentId, + if (value != null) 'threshold': value, + } + })); + } } diff --git a/apps/flutter_parent/lib/network/api/announcement_api.dart b/apps/flutter_parent/lib/network/api/announcement_api.dart index e8f7d50e8f..3fcab50f4f 100644 --- a/apps/flutter_parent/lib/network/api/announcement_api.dart +++ b/apps/flutter_parent/lib/network/api/announcement_api.dart @@ -18,12 +18,13 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class AnnouncementApi { - Future getCourseAnnouncement(String courseId, String announcementId, bool forceRefresh) { - return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/$courseId/discussion_topics/$announcementId')); + Future getCourseAnnouncement(String courseId, String announcementId, bool forceRefresh) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/$courseId/discussion_topics/$announcementId')); } - Future getAccountNotification(String accountNotificationId, bool forceRefresh) { - return fetch(canvasDio(forceRefresh: forceRefresh) - .get('accounts/self/users/self/account_notifications/$accountNotificationId')); + Future getAccountNotification(String accountNotificationId, bool forceRefresh) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('accounts/self/users/self/account_notifications/$accountNotificationId')); } } diff --git a/apps/flutter_parent/lib/network/api/assignment_api.dart b/apps/flutter_parent/lib/network/api/assignment_api.dart index 1e268e29b3..aaa920bf40 100644 --- a/apps/flutter_parent/lib/network/api/assignment_api.dart +++ b/apps/flutter_parent/lib/network/api/assignment_api.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/network/utils/fetch.dart'; import 'package:flutter_parent/network/utils/paged_list.dart'; class AssignmentApi { - Future> getAssignmentsWithSubmissionsDepaginated(int courseId, int studentId) async { + Future?> getAssignmentsWithSubmissionsDepaginated(int courseId, int studentId) async { var dio = canvasDio(); var params = { 'include[]': ['all_dates', 'overrides', 'rubric_assessment', 'submission'], @@ -30,8 +30,8 @@ class AssignmentApi { return fetchList(dio.get('courses/$courseId/assignments', queryParameters: params), depaginateWith: dio); } - Future> getAssignmentGroupsWithSubmissionsDepaginated( - String courseId, String studentId, String gradingPeriodId, + Future?> getAssignmentGroupsWithSubmissionsDepaginated( + String courseId, String? studentId, String? gradingPeriodId, {bool forceRefresh = false}) async { var dio = canvasDio(forceRefresh: forceRefresh); var params = { @@ -49,24 +49,25 @@ class AssignmentApi { return fetchList(dio.get('courses/$courseId/assignment_groups', queryParameters: params), depaginateWith: dio); } - Future> getAssignmentsWithSubmissionsPaged(String courseId, String studentId) async { + Future?> getAssignmentsWithSubmissionsPaged(String courseId, String studentId) async { var params = { 'include[]': ['all_dates', 'overrides', 'rubric_assessment', 'submission'], 'order_by': 'due_at', 'override_assignment_dates': 'true', 'needs_grading_count_by_section': 'true', }; - return fetchFirstPage(canvasDio().get('courses/$courseId/assignments', queryParameters: params)); + var dio = canvasDio(); + return fetchFirstPage(dio.get('courses/$courseId/assignments', queryParameters: params)); } - Future getAssignment(String courseId, String assignmentId, {bool forceRefresh = false}) async { + Future getAssignment(String courseId, String assignmentId, {bool forceRefresh = false}) async { var params = { 'include[]': ['overrides', 'rubric_assessment', 'submission', 'observed_users'], 'all_dates': 'true', 'override_assignment_dates': 'true', 'needs_grading_count_by_section': 'true', }; - return fetch(canvasDio(forceRefresh: forceRefresh) - .get('courses/$courseId/assignments/$assignmentId', queryParameters: params)); + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/$courseId/assignments/$assignmentId', queryParameters: params)); } } diff --git a/apps/flutter_parent/lib/network/api/auth_api.dart b/apps/flutter_parent/lib/network/api/auth_api.dart index c82fe50971..dbfbc62faf 100644 --- a/apps/flutter_parent/lib/network/api/auth_api.dart +++ b/apps/flutter_parent/lib/network/api/auth_api.dart @@ -21,7 +21,7 @@ import 'package:flutter_parent/network/utils/fetch.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; class AuthApi { - Future refreshToken() async { + Future refreshToken() async { var dio = DioConfig.canvas(includeApiPath: false).dio; var params = { 'client_id': ApiPrefs.getClientId(), @@ -33,18 +33,18 @@ class AuthApi { return fetch(dio.post('login/oauth2/token', data: params)); } - Future getTokens(MobileVerifyResult verifyResult, String requestCode) async { + Future getTokens(MobileVerifyResult? verifyResult, String requestCode) async { var dio = DioConfig().dio; var params = { - 'client_id': verifyResult.clientId, - 'client_secret': verifyResult.clientSecret, + 'client_id': verifyResult?.clientId, + 'client_secret': verifyResult?.clientSecret, 'code': requestCode, 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', }; - return fetch(dio.post('${verifyResult.baseUrl}/login/oauth2/token', data: params)); + return fetch(dio.post('${verifyResult?.baseUrl}/login/oauth2/token', data: params)); } - Future mobileVerify(String domain, {bool forceBetaDomain = false}) async { + Future mobileVerify(String domain, {bool forceBetaDomain = false}) async { // We only want to switch over to the beta mobile verify domain if either: // (1) we are forcing the beta domain (typically in UI tests) OR // (2) the remote firebase config setting for MOBILE_VERIFY_BETA_ENABLED is true diff --git a/apps/flutter_parent/lib/network/api/calendar_events_api.dart b/apps/flutter_parent/lib/network/api/calendar_events_api.dart index 03aa1e75b4..e9e70143a5 100644 --- a/apps/flutter_parent/lib/network/api/calendar_events_api.dart +++ b/apps/flutter_parent/lib/network/api/calendar_events_api.dart @@ -17,14 +17,14 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class CalendarEventsApi { - Future> getAllCalendarEvents({ + Future?> getAllCalendarEvents({ bool allEvents = false, String type = ScheduleItem.apiTypeCalendar, - String startDate = null, - String endDate = null, + String? startDate = null, + String? endDate = null, List contexts = const [], bool forceRefresh = false, - }) { + }) async { var dio = canvasDio(forceRefresh: forceRefresh, pageSize: PageSize.canvasMax); var params = { 'all_events': allEvents, @@ -36,10 +36,12 @@ class CalendarEventsApi { return fetchList(dio.get('calendar_events', queryParameters: params), depaginateWith: dio); } - Future getEvent(String eventId, bool forceRefresh) => - fetch(canvasDio(forceRefresh: forceRefresh).get('calendar_events/$eventId')); + Future getEvent(String? eventId, bool forceRefresh) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('calendar_events/$eventId')); + } - Future> getUserCalendarItems( + Future?> getUserCalendarItems( String userId, DateTime startDay, DateTime endDay, diff --git a/apps/flutter_parent/lib/network/api/course_api.dart b/apps/flutter_parent/lib/network/api/course_api.dart index 9ce9f502c7..1870c74f56 100644 --- a/apps/flutter_parent/lib/network/api/course_api.dart +++ b/apps/flutter_parent/lib/network/api/course_api.dart @@ -21,7 +21,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class CourseApi { - Future> getObserveeCourses({bool forceRefresh: false}) async { + Future?> getObserveeCourses({bool forceRefresh = false}) async { final dio = canvasDio(forceRefresh: forceRefresh, pageSize: PageSize.canvasMax); final params = { 'include[]': [ @@ -45,7 +45,7 @@ class CourseApi { return fetchList(dio.get('courses', queryParameters: params), depaginateWith: dio); } - Future getCourse(String courseId, {bool forceRefresh: false}) async { + Future getCourse(String courseId, {bool forceRefresh = false}) async { final params = { 'include[]': [ 'syllabus_body', @@ -62,23 +62,28 @@ class CourseApi { 'grading_scheme' ] }; - return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/${courseId}', queryParameters: params)); + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/${courseId}', queryParameters: params)); } // TODO: Set up pagination when API is fixed (no header link) and remove per_page query parameter - Future getGradingPeriods(String courseId, {bool forceRefresh = false}) { - return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/$courseId/grading_periods?per_page=100')); + Future getGradingPeriods(String courseId, {bool forceRefresh = false}) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/$courseId/grading_periods?per_page=100')); } - Future> getCourseTabs(String courseId, {bool forceRefresh}) { - return fetchList(canvasDio(forceRefresh: forceRefresh).get('courses/$courseId/tabs')); + Future?> getCourseTabs(String courseId, {bool forceRefresh = false}) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetchList(dio.get('courses/$courseId/tabs')); } - Future getCourseSettings(String courseId, {bool forceRefresh}) { - return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/$courseId/settings')); + Future getCourseSettings(String courseId, {bool forceRefresh = false}) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/$courseId/settings')); } - Future getCoursePermissions(String courseId, {bool forceRefresh = false}) { - return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/$courseId/permissions')); + Future getCoursePermissions(String courseId, {bool forceRefresh = false}) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/$courseId/permissions')); } } diff --git a/apps/flutter_parent/lib/network/api/enrollments_api.dart b/apps/flutter_parent/lib/network/api/enrollments_api.dart index c373b780fe..230385bb58 100644 --- a/apps/flutter_parent/lib/network/api/enrollments_api.dart +++ b/apps/flutter_parent/lib/network/api/enrollments_api.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class EnrollmentsApi { - Future> getObserveeEnrollments({bool forceRefresh = false}) async { + Future?> getObserveeEnrollments({bool forceRefresh = false}) async { var dio = canvasDio(pageSize: PageSize.canvasMax, forceRefresh: forceRefresh); var params = { 'include[]': ['observed_users', 'avatar_url'], @@ -28,7 +28,7 @@ class EnrollmentsApi { return fetchList(dio.get('users/self/enrollments', queryParameters: params), depaginateWith: dio); } - Future> getSelfEnrollments({bool forceRefresh = false}) async { + Future?> getSelfEnrollments({bool forceRefresh = false}) async { var dio = canvasDio(pageSize: PageSize.canvasMax, forceRefresh: forceRefresh); var params = { 'state[]': ['creation_pending', 'invited', 'active', 'completed'] @@ -36,8 +36,8 @@ class EnrollmentsApi { return fetchList(dio.get('users/self/enrollments', queryParameters: params), depaginateWith: dio); } - Future> getEnrollmentsByGradingPeriod(String courseId, String studentId, String gradingPeriodId, - {bool forceRefresh = false}) { + Future?> getEnrollmentsByGradingPeriod(String courseId, String? studentId, String? gradingPeriodId, + {bool forceRefresh = false}) async { final dio = canvasDio(forceRefresh: forceRefresh); final params = { 'state[]': ['active', 'completed'], // current_and_concluded state not supported for observers @@ -49,29 +49,31 @@ class EnrollmentsApi { dio.get( 'courses/$courseId/enrollments', queryParameters: params, - options: Options(validateStatus: (status) => status < 500)), // Workaround, because this request fails for some legacy users, but we can't catch the error. + options: Options(validateStatus: (status) => status != null && status < 500)), // Workaround, because this request fails for some legacy users, but we can't catch the error. depaginateWith: dio, ); } /// Attempts to pair a student and observer using the given pairing code. The returned future will produce true if /// successful, false if the code is invalid or expired, and null if there was a network issue. - Future pairWithStudent(String pairingCode) async { + Future pairWithStudent(String pairingCode) async { try { - var pairingResponse = await canvasDio().post(ApiPrefs.getApiUrl(path: 'users/${ApiPrefs.getUser().id}/observees'), + var dio = canvasDio(); + var pairingResponse = await dio.post(ApiPrefs.getApiUrl(path: 'users/${ApiPrefs.getUser()?.id}/observees'), queryParameters: {'pairing_code': pairingCode}); return (pairingResponse.statusCode == 200 || pairingResponse.statusCode == 201); } on DioError catch (e) { // The API returns status code 422 on pairing failure - if (e.type == DioErrorType.response && e.response.statusCode == 422) return false; + if (e.response?.statusCode == 422) return false; return null; } } Future unpairStudent(String studentId) async { try { - var response = await canvasDio().delete( - ApiPrefs.getApiUrl(path: 'users/${ApiPrefs.getUser().id}/observees/$studentId'), + var dio = canvasDio(); + var response = await dio.delete( + ApiPrefs.getApiUrl(path: 'users/${ApiPrefs.getUser()?.id}/observees/$studentId'), ); return (response.statusCode == 200 || response.statusCode == 201); } on DioError { @@ -81,8 +83,9 @@ class EnrollmentsApi { Future canUnpairStudent(String studentId) async { try { - var response = await canvasDio().get( - ApiPrefs.getApiUrl(path: 'users/${ApiPrefs.getUser().id}/observees/$studentId'), + var dio = canvasDio(); + var response = await dio.get( + ApiPrefs.getApiUrl(path: 'users/${ApiPrefs.getUser()?.id}/observees/$studentId'), ); return response.statusCode == 200; } on DioError { diff --git a/apps/flutter_parent/lib/network/api/error_report_api.dart b/apps/flutter_parent/lib/network/api/error_report_api.dart index 183e4af791..1b7650dbbf 100644 --- a/apps/flutter_parent/lib/network/api/error_report_api.dart +++ b/apps/flutter_parent/lib/network/api/error_report_api.dart @@ -18,19 +18,21 @@ class ErrorReportApi { static const DEFAULT_DOMAIN = 'https://canvas.instructure.com'; Future submitErrorReport({ - String subject, - String description, - String email, - String severity, - String stacktrace, - String domain, - String name, - String becomeUser, - String userRoles, - }) { + String? subject, + String? description, + String? email, + String? severity, + String? stacktrace, + String? domain, + String? name, + String? becomeUser, + String? userRoles, + }) async { var config = domain == DEFAULT_DOMAIN ? DioConfig.core() : DioConfig.canvas(); - return config.dio.post( + var dio = config.dio; + + await dio.post( '/error_reports.json', queryParameters: { 'error[subject]': subject, diff --git a/apps/flutter_parent/lib/network/api/features_api.dart b/apps/flutter_parent/lib/network/api/features_api.dart index e5b633f706..229615d53f 100644 --- a/apps/flutter_parent/lib/network/api/features_api.dart +++ b/apps/flutter_parent/lib/network/api/features_api.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/network/utils/fetch.dart'; class FeaturesApi { - Future getFeatureFlags() { + Future getFeatureFlags() async { var dio = canvasDio(forceRefresh: true); return fetch(dio.get('/features/environment')); diff --git a/apps/flutter_parent/lib/network/api/file_api.dart b/apps/flutter_parent/lib/network/api/file_api.dart index b79d975025..8e6139ce9d 100644 --- a/apps/flutter_parent/lib/network/api/file_api.dart +++ b/apps/flutter_parent/lib/network/api/file_api.dart @@ -28,7 +28,7 @@ class FileApi { /// 'total' value of -1 is considered to represent indeterminate progress, and means either the file size is unknown /// or the upload is at a stage where progress cannot be determined. In either case, user-facing progress indicators /// should be aware of this and show 'indeterminate' progress as needed. - Future uploadConversationFile(File file, ProgressCallback progressCallback) async { + Future uploadConversationFile(File file, ProgressCallback progressCallback) async { progressCallback(0, -1); // Indeterminate final name = basename(file.path); final size = await file.length(); @@ -44,7 +44,7 @@ class FileApi { }); // Get the upload configuration - FileUploadConfig uploadConfig; + FileUploadConfig? uploadConfig; try { uploadConfig = await fetch(dio.post('users/self/files', queryParameters: params)); } catch (e) { @@ -53,12 +53,12 @@ class FileApi { } // Build the form data for upload - FormData formData = FormData.fromMap(uploadConfig.params.toMap()); + FormData formData = FormData.fromMap(uploadConfig?.params?.toMap() ?? {}); formData.files.add(MapEntry('file', await MultipartFile.fromFile(file.path, filename: name))); // Perform upload with progress return fetch(Dio().post( - uploadConfig.url, + uploadConfig?.url ?? '', data: formData, onSendProgress: (count, total) { if (total > 0 && count >= total) { @@ -75,10 +75,11 @@ class FileApi { Future downloadFile( String url, String savePath, { - CancelToken cancelToken, - ProgressCallback onProgress, + CancelToken? cancelToken, + ProgressCallback? onProgress, }) async { - await DioConfig.core(forceRefresh: true).dio.download( + var dio = DioConfig.core(forceRefresh: true).dio; + await dio.download( url, savePath, cancelToken: cancelToken, @@ -87,5 +88,8 @@ class FileApi { return File(savePath); } - Future deleteFile(String fileId) => canvasDio().delete('files/$fileId'); + Future deleteFile(String fileId) async { + var dio = canvasDio(); + await dio.delete('files/$fileId'); + } } diff --git a/apps/flutter_parent/lib/network/api/heap_api.dart b/apps/flutter_parent/lib/network/api/heap_api.dart index eae8020e98..7fb61d389e 100644 --- a/apps/flutter_parent/lib/network/api/heap_api.dart +++ b/apps/flutter_parent/lib/network/api/heap_api.dart @@ -14,7 +14,6 @@ import 'dart:convert'; -import 'package:dio/dio.dart'; import 'package:encrypt/encrypt.dart' as encrypt; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/network/utils/dio_config.dart'; @@ -26,7 +25,8 @@ class HeapApi { final currentLogin = ApiPrefs.getCurrentLogin(); if (currentLogin == null) return false; - final userId = ApiPrefs.getCurrentLogin().user.id; + final userId = ApiPrefs.getCurrentLogin()?.user.id; + if (userId == null) return false; final encrypter = encrypt.Encrypter(encrypt.AES(encrypt.Key.fromUtf8(ENCRYPT_KEY))); final encryptedId = encrypter.encrypt(userId, iv: encrypt.IV.fromUtf8(ENCRYPT_IV)).base64; @@ -41,7 +41,8 @@ class HeapApi { data['properties'] = json.encode(extras); } - final response = await heapDio.dio.post('/track', data: data); + var dio = heapDio.dio; + final response = await dio.post('/track', data: data); return response.statusCode == 200; } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/network/api/help_links_api.dart b/apps/flutter_parent/lib/network/api/help_links_api.dart index b6217a9c1e..6d33f44659 100644 --- a/apps/flutter_parent/lib/network/api/help_links_api.dart +++ b/apps/flutter_parent/lib/network/api/help_links_api.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/network/utils/fetch.dart'; const String _helpLinksEndpoint = 'accounts/self/help_links'; class HelpLinksApi { - Future getHelpLinks({forceRefresh = false}) { + Future getHelpLinks({forceRefresh = false}) async { var dio = canvasDio(forceRefresh: forceRefresh, pageSize: PageSize.canvasMax); return fetch(dio.get(_helpLinksEndpoint)); } diff --git a/apps/flutter_parent/lib/network/api/inbox_api.dart b/apps/flutter_parent/lib/network/api/inbox_api.dart index 13ed9efdf5..ac5ab649b9 100644 --- a/apps/flutter_parent/lib/network/api/inbox_api.dart +++ b/apps/flutter_parent/lib/network/api/inbox_api.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class InboxApi { - Future> getConversations({String scope: null, bool forceRefresh: false}) async { + Future?> getConversations({String? scope = null, bool forceRefresh = false}) async { final dio = canvasDio(forceRefresh: forceRefresh, pageSize: PageSize.canvasMax); final params = { 'scope': scope, @@ -28,64 +28,69 @@ class InboxApi { return fetchList(dio.get('conversations', queryParameters: params), depaginateWith: dio); } - Future getConversation(String id, {bool refresh: false}) { - return fetch(canvasDio(forceRefresh: refresh).get('conversations/$id')); + Future getConversation(String id, {bool refresh = false}) async { + var dio = canvasDio(forceRefresh: refresh); + return fetch(dio.get('conversations/$id')); } - Future getUnreadCount() => fetch(canvasDio(forceRefresh: true).get('conversations/unread_count')); - - Future addMessage( - String conversationId, - String body, - List recipientIds, - List attachmentIds, - List includeMessageIds, - ) async { - var config = DioConfig.canvas(); - Conversation conversation = await fetch( - config.dio.post( - 'conversations/$conversationId/add_message', - queryParameters: { - 'body': body, - 'recipients[]': recipientIds, - 'attachment_ids[]': attachmentIds, - 'included_messages[]': includeMessageIds, - }, - ), - ); - config.clearCache(path: 'conversations'); - config.clearCache(path: 'conversations/$conversationId'); - return conversation; + Future getUnreadCount() async { + var dio = canvasDio(forceRefresh: true); + return fetch(dio.get('conversations/unread_count')); } - Future> getRecipients(String courseId, {bool forceRefresh: false}) { - var dio = canvasDio(forceRefresh: forceRefresh, pageSize: PageSize.canvasMax); - var params = { - 'permissions[]': ['send_messages_all'], - 'messageable_only': true, - 'context': 'course_$courseId', - }; - return fetchList(dio.get('search/recipients', queryParameters: params), depaginateWith: dio); - } + Future addMessage( + String? conversationId, + String body, + List recipientIds, + List attachmentIds, + List includeMessageIds, + ) async { + var config = DioConfig.canvas(); + var dio = config.dio; + Conversation? conversation = await fetch( + dio.post( + 'conversations/$conversationId/add_message', + queryParameters: { + 'body': body, + 'recipients[]': recipientIds, + 'attachment_ids[]': attachmentIds, + 'included_messages[]': includeMessageIds, + }, + ), + ); + config.clearCache(path: 'conversations'); + config.clearCache(path: 'conversations/$conversationId'); + return conversation; + } - Future createConversation( - String courseId, - List recipientIds, - String subject, - String body, - List attachmentIds, - ) async { - var dio = canvasDio(); - var params = { - 'group_conversation': 'true', - 'recipients[]': recipientIds, - 'context_code': 'course_$courseId', - 'subject': subject, - 'body': body, - 'attachment_ids[]': attachmentIds, - }; - List result = await fetchList(dio.post('conversations', queryParameters: params)); - DioConfig.canvas().clearCache(path: 'conversations'); - return result[0]; + Future?> getRecipients(String courseId, {bool forceRefresh = false}) async { + var dio = canvasDio(forceRefresh: forceRefresh, pageSize: PageSize.canvasMax); + var params = { + 'permissions[]': ['send_messages_all'], + 'messageable_only': true, + 'context': 'course_$courseId', + }; + return fetchList(dio.get('search/recipients', queryParameters: params), depaginateWith: dio); + } + + Future createConversation( + String courseId, + List recipientIds, + String subject, + String body, + List? attachmentIds, + ) async { + var dio = canvasDio(); + var params = { + 'group_conversation': 'true', + 'recipients[]': recipientIds, + 'context_code': 'course_$courseId', + 'subject': subject, + 'body': body, + 'attachment_ids[]': attachmentIds, + }; + List? result = await fetchList(dio.post('conversations', queryParameters: params)); + DioConfig.canvas().clearCache(path: 'conversations'); + return result?[0]; + } } -} diff --git a/apps/flutter_parent/lib/network/api/oauth_api.dart b/apps/flutter_parent/lib/network/api/oauth_api.dart index dbb63ee312..f6e8c06d09 100644 --- a/apps/flutter_parent/lib/network/api/oauth_api.dart +++ b/apps/flutter_parent/lib/network/api/oauth_api.dart @@ -17,7 +17,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class OAuthApi { - Future getAuthenticatedUrl(String targetUrl) { + Future getAuthenticatedUrl(String targetUrl) async { final dio = canvasDio(forceRefresh: true, includeApiPath: false); final params = {'return_to': targetUrl}; return fetch(dio.get('login/session_token', queryParameters: params)); diff --git a/apps/flutter_parent/lib/network/api/page_api.dart b/apps/flutter_parent/lib/network/api/page_api.dart index cd82430f35..d5048187eb 100644 --- a/apps/flutter_parent/lib/network/api/page_api.dart +++ b/apps/flutter_parent/lib/network/api/page_api.dart @@ -17,7 +17,8 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class PageApi { - Future getCourseFrontPage(String courseId, {bool forceRefresh = false}) async { - return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/$courseId/front_page')); + Future getCourseFrontPage(String courseId, {bool forceRefresh = false}) async { + var dio = canvasDio(forceRefresh: forceRefresh); + return fetch(dio.get('courses/$courseId/front_page')); } } diff --git a/apps/flutter_parent/lib/network/api/planner_api.dart b/apps/flutter_parent/lib/network/api/planner_api.dart index cdf920948e..a5dfd46344 100644 --- a/apps/flutter_parent/lib/network/api/planner_api.dart +++ b/apps/flutter_parent/lib/network/api/planner_api.dart @@ -17,7 +17,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class PlannerApi { - Future> getUserPlannerItems( + Future?> getUserPlannerItems( String userId, DateTime startDay, DateTime endDay, { diff --git a/apps/flutter_parent/lib/network/api/user_api.dart b/apps/flutter_parent/lib/network/api/user_api.dart index b17a61177f..2fcd2eded2 100644 --- a/apps/flutter_parent/lib/network/api/user_api.dart +++ b/apps/flutter_parent/lib/network/api/user_api.dart @@ -22,31 +22,39 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class UserApi { - Future getSelf() => fetch(canvasDio(forceDeviceLanguage: true, forceRefresh: true).get('users/self/profile')); + Future getSelf() async { + var dio = canvasDio(forceDeviceLanguage: true, forceRefresh: true); + return fetch(dio.get('users/self/profile')); + } - Future getUserForDomain(String domain, String userId) async { + Future getUserForDomain(String domain, String userId) async { var dio = DioConfig.canvas().copyWith(baseUrl: '$domain/api/v1/').dio; return fetch(dio.get('users/$userId/profile')); } - Future getSelfPermissions() => - fetch(canvasDio(forceRefresh: true).get('users/self')).then((user) => user.permissions); + Future getSelfPermissions() async { + var dio = canvasDio(forceRefresh: true); + return fetch(dio.get('users/self')).then((user) => user?.permissions); + } - Future getUserColors({bool refresh = false}) async { - return fetch(canvasDio(forceRefresh: refresh).get('users/self/colors')); + Future getUserColors({bool refresh = false}) async { + var dio = canvasDio(forceRefresh: refresh); + return fetch(dio.get('users/self/colors')); } - Future acceptUserTermsOfUse() async { + Future acceptUserTermsOfUse() async { final queryParams = {'user[terms_of_use]': 1}; - return fetch(canvasDio().put('users/self', queryParameters: queryParams)); + var dio = canvasDio(); + return fetch(dio.put('users/self', queryParameters: queryParams)); } - Future setUserColor(String contextId, Color color) async { + Future setUserColor(String contextId, Color color) async { var hexCode = '#' + color.value.toRadixString(16).substring(2); var queryParams = {'hexcode': hexCode}; - return fetch(canvasDio().put( + var dio = canvasDio(); + return fetch(dio.put( 'users/self/colors/$contextId', queryParameters: queryParams, - options: Options(validateStatus: (status) => status < 500))); // Workaround, because this request fails for some legacy users, but we can't catch the error.)); + options: Options(validateStatus: (status) => status != null && status < 500))); // Workaround, because this request fails for some legacy users, but we can't catch the error.)); } } diff --git a/apps/flutter_parent/lib/network/utils/analytics.dart b/apps/flutter_parent/lib/network/utils/analytics.dart index 55d8e4d0e4..9254d431a0 100644 --- a/apps/flutter_parent/lib/network/utils/analytics.dart +++ b/apps/flutter_parent/lib/network/utils/analytics.dart @@ -11,13 +11,11 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:device_info/device_info.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_parent/network/api/heap_api.dart'; import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; -import 'package:flutter_parent/utils/service_locator.dart'; /// Event names /// The naming scheme for the majority of these is found in a google doc so that we can be consistent diff --git a/apps/flutter_parent/lib/network/utils/analytics_observer.dart b/apps/flutter_parent/lib/network/utils/analytics_observer.dart index ddc60492fd..5ab9609671 100644 --- a/apps/flutter_parent/lib/network/utils/analytics_observer.dart +++ b/apps/flutter_parent/lib/network/utils/analytics_observer.dart @@ -21,18 +21,18 @@ import 'package:flutter_parent/utils/service_locator.dart'; class AnalyticsObserver extends NavigatorObserver { void _sendScreenView(PageRoute route) { if (route.settings.name == null) return; // No name means we can't match it, should be logged by QuickNav.push - final match = PandaRouter.router.match(route.settings.name); - final String screenName = match.route.route; + final match = PandaRouter.router.match(route.settings.name!); + final String? screenName = match?.route.route; if (screenName != null) { final message = - 'Pushing widget: $screenName ${match.parameters.isNotEmpty ? 'with params: ${match.parameters}' : ''}'; + 'Pushing widget: $screenName ${match!.parameters.isNotEmpty ? 'with params: ${match.parameters}' : ''}'; locator().logMessage(message); // Log message for crashlytics debugging locator().setCurrentScreen(screenName); // Log current screen for analytics } } @override - void didPush(Route route, Route previousRoute) { + void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); if (route is PageRoute) { _sendScreenView(route); @@ -40,7 +40,7 @@ class AnalyticsObserver extends NavigatorObserver { } @override - void didReplace({Route newRoute, Route oldRoute}) { + void didReplace({Route? newRoute, Route? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); if (newRoute is PageRoute) { _sendScreenView(newRoute); @@ -48,7 +48,7 @@ class AnalyticsObserver extends NavigatorObserver { } @override - void didPop(Route route, Route previousRoute) { + void didPop(Route route, Route? previousRoute) { super.didPop(route, previousRoute); if (previousRoute is PageRoute && route is PageRoute) { _sendScreenView(previousRoute); diff --git a/apps/flutter_parent/lib/network/utils/api_prefs.dart b/apps/flutter_parent/lib/network/utils/api_prefs.dart index a4fb8d0a99..ae40d47d5e 100644 --- a/apps/flutter_parent/lib/network/utils/api_prefs.dart +++ b/apps/flutter_parent/lib/network/utils/api_prefs.dart @@ -16,6 +16,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:collection/collection.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/models/login.dart'; @@ -29,7 +30,7 @@ import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:intl/intl.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; @@ -48,9 +49,9 @@ class ApiPrefs { static const String KEY_LAST_ACCOUNT = 'last_account'; static const String KEY_LAST_ACCOUNT_LOGIN_FLOW = 'last_account_login_flow'; - static EncryptedSharedPreferences _prefs; - static PackageInfo _packageInfo; - static Login _currentLogin; + static EncryptedSharedPreferences? _prefs; + static PackageInfo? _packageInfo; + static Login? _currentLogin; static Future init() async { if (_prefs == null) _prefs = await EncryptedSharedPreferences.getInstance(); @@ -58,33 +59,33 @@ class ApiPrefs { await _migrateToEncryptedPrefs(); } - static void _migrateToEncryptedPrefs() async { - if (_prefs.getBool(KEY_HAS_MIGRATED_TO_ENCRYPTED_PREFS) ?? false) { + static Future _migrateToEncryptedPrefs() async { + if (_prefs?.getBool(KEY_HAS_MIGRATED_TO_ENCRYPTED_PREFS) ?? false) { return; } // Set the bool flag so we don't migrate multiple times - await _prefs.setBool(KEY_HAS_MIGRATED_TO_ENCRYPTED_PREFS, true); + await _prefs?.setBool(KEY_HAS_MIGRATED_TO_ENCRYPTED_PREFS, true); final oldPrefs = await SharedPreferences.getInstance(); - await _prefs.setStringList(KEY_LOGINS, oldPrefs.getStringList(KEY_LOGINS)); + await _prefs?.setStringList(KEY_LOGINS, oldPrefs.getStringList(KEY_LOGINS)); await oldPrefs.remove(KEY_LOGINS); - await _prefs.setBool(KEY_HAS_MIGRATED, oldPrefs.getBool(KEY_HAS_MIGRATED)); + await _prefs?.setBool(KEY_HAS_MIGRATED, oldPrefs.getBool(KEY_HAS_MIGRATED)); await oldPrefs.remove(KEY_HAS_MIGRATED); - await _prefs.setBool(KEY_HAS_CHECKED_OLD_REMINDERS, oldPrefs.getBool(KEY_HAS_CHECKED_OLD_REMINDERS)); + await _prefs?.setBool(KEY_HAS_CHECKED_OLD_REMINDERS, oldPrefs.getBool(KEY_HAS_CHECKED_OLD_REMINDERS)); await oldPrefs.remove(KEY_HAS_CHECKED_OLD_REMINDERS); - await _prefs.setString(KEY_CURRENT_LOGIN_UUID, oldPrefs.getString(KEY_CURRENT_LOGIN_UUID)); + await _prefs?.setString(KEY_CURRENT_LOGIN_UUID, oldPrefs.getString(KEY_CURRENT_LOGIN_UUID)); await oldPrefs.remove(KEY_CURRENT_LOGIN_UUID); - await _prefs.setString(KEY_CURRENT_STUDENT, oldPrefs.getString(KEY_CURRENT_STUDENT)); + await _prefs?.setString(KEY_CURRENT_STUDENT, oldPrefs.getString(KEY_CURRENT_STUDENT)); await oldPrefs.remove(KEY_CURRENT_STUDENT); } - static void clean() { + static Future clean() async { _prefs?.clear(); _prefs = null; _packageInfo = null; @@ -104,11 +105,11 @@ class ApiPrefs { return token.isNotEmpty && domain.isNotEmpty; } - static Login getCurrentLogin() { + static Login? getCurrentLogin() { _checkInit(); if (_currentLogin == null) { final currentLoginUuid = getCurrentLoginUuid(); - _currentLogin = getLogins().firstWhere((it) => it.uuid == currentLoginUuid, orElse: () => null); + _currentLogin = getLogins().firstWhereOrNull((it) => it.uuid == currentLoginUuid); } return _currentLogin; } @@ -116,7 +117,7 @@ class ApiPrefs { static Future switchLogins(Login login) async { _checkInit(); _currentLogin = login; - await _prefs.setString(KEY_CURRENT_LOGIN_UUID, login.uuid); + await _prefs?.setString(KEY_CURRENT_LOGIN_UUID, login.uuid); } static bool isMasquerading() { @@ -135,20 +136,20 @@ class ApiPrefs { if (!switchingLogins) { // Remove reminders ReminderDb reminderDb = locator(); - final reminders = await reminderDb.getAllForUser(getDomain(), getUser().id); - final reminderIds = reminders?.map((it) => it.id)?.toList(); + final reminders = await reminderDb.getAllForUser(getDomain(), getUser()?.id); + final reminderIds = reminders?.map((it) => it.id).toList().nonNulls.toList() ?? []; await locator().deleteNotifications(reminderIds); - await reminderDb.deleteAllForUser(getDomain(), getUser().id); + await reminderDb.deleteAllForUser(getDomain(), getUser()?.id); // Remove calendar filters - locator().deleteAllForUser(getDomain(), getUser().id); + locator().deleteAllForUser(getDomain(), getUser()?.id); // Remove saved Login data await removeLoginByUuid(getCurrentLoginUuid()); } // Clear current Login - await _prefs.remove(KEY_CURRENT_LOGIN_UUID); + await _prefs!.remove(KEY_CURRENT_LOGIN_UUID); _currentLogin = null; app?.rebuild(effectiveLocale()); } @@ -156,7 +157,7 @@ class ApiPrefs { static Future saveLogins(List logins) async { _checkInit(); List jsonList = logins.map((it) => json.encode(serialize(it))).toList(); - await _prefs.setStringList(KEY_LOGINS, jsonList); + await _prefs!.setStringList(KEY_LOGINS, jsonList); } static Future addLogin(Login login) async { @@ -169,35 +170,38 @@ class ApiPrefs { static List getLogins() { _checkInit(); - return _prefs.getStringList(KEY_LOGINS)?.map((it) => deserialize(json.decode(it)))?.toList() ?? []; + var list = _prefs!.getStringList(KEY_LOGINS); + return list.map((it) => deserialize(json.decode(it))).nonNulls.toList(); } static setLastAccount(SchoolDomain lastAccount, LoginFlow loginFlow) { _checkInit(); final lastAccountJson = json.encode(serialize(lastAccount)); - _prefs.setString(KEY_LAST_ACCOUNT, lastAccountJson); - _prefs.setInt(KEY_LAST_ACCOUNT_LOGIN_FLOW, loginFlow.index); + _prefs!.setString(KEY_LAST_ACCOUNT, lastAccountJson); + _prefs!.setInt(KEY_LAST_ACCOUNT_LOGIN_FLOW, loginFlow.index); } - static Tuple2 getLastAccount() { + static Tuple2? getLastAccount() { _checkInit(); - if (!_prefs.containsKey(KEY_LAST_ACCOUNT)) return null; + if (!_prefs!.containsKey(KEY_LAST_ACCOUNT)) return null; - final accountJson = _prefs.getString(KEY_LAST_ACCOUNT); - if (accountJson == null || accountJson.isEmpty) return null; + final accountJson = _prefs!.getString(KEY_LAST_ACCOUNT); + if (accountJson == null || accountJson.isEmpty == true) return null; final lastAccount = deserialize(json.decode(accountJson)); - final loginFlow = _prefs.containsKey(KEY_LAST_ACCOUNT_LOGIN_FLOW) ? LoginFlow.values[_prefs.getInt(KEY_LAST_ACCOUNT_LOGIN_FLOW)] : LoginFlow.normal; + int? lastLogin = _prefs!.getInt(KEY_LAST_ACCOUNT_LOGIN_FLOW); + if (lastLogin == null) return null; + final loginFlow = _prefs!.containsKey(KEY_LAST_ACCOUNT_LOGIN_FLOW) ? LoginFlow.values[lastLogin] : LoginFlow.normal; - return Tuple2(lastAccount, loginFlow); + return Tuple2(lastAccount!, loginFlow); } static Future removeLogin(Login login) => removeLoginByUuid(login.uuid); - static Future removeLoginByUuid(String uuid) async { + static Future removeLoginByUuid(String? uuid) async { _checkInit(); var logins = getLogins(); - Login login = logins.firstWhere((it) => it.uuid == uuid, orElse: () => null); + Login? login = logins.firstWhereOrNull((it) => it.uuid == uuid); if (login != null) { // Delete token (fire and forget - no need to await) locator().deleteToken(login.domain, login.accessToken); @@ -241,9 +245,9 @@ class ApiPrefs { } } - static Locale effectiveLocale() { + static Locale? effectiveLocale() { _checkInit(); - User user = getUser(); + User? user = getUser(); List userLocale = (user?.effectiveLocale ?? user?.locale ?? ui.window.locale.toLanguageTag()).split('-x-'); if (userLocale[0].isEmpty) { @@ -269,88 +273,97 @@ class ApiPrefs { /// Prefs - static String getCurrentLoginUuid() => _getPrefString(KEY_CURRENT_LOGIN_UUID); + static String? getCurrentLoginUuid() => _getPrefString(KEY_CURRENT_LOGIN_UUID); - static User getUser() => getCurrentLogin()?.currentUser; + static User? getUser() => getCurrentLogin()?.currentUser; - static String getUserAgent() => 'androidParent/${_packageInfo.version} (${_packageInfo.buildNumber})'; + static String getUserAgent() => 'androidParent/${_packageInfo?.version} (${_packageInfo?.buildNumber})'; static String getApiUrl({String path = ''}) => '${getDomain()}/api/v1/$path'; - static String getDomain() => getCurrentLogin()?.currentDomain; + static String? getDomain() => getCurrentLogin()?.currentDomain; - static String getAuthToken() => getCurrentLogin()?.accessToken; + static String? getAuthToken() => getCurrentLogin()?.accessToken; - static String getRefreshToken() => getCurrentLogin()?.refreshToken; + static String? getRefreshToken() => getCurrentLogin()?.refreshToken; - static String getClientId() => getCurrentLogin()?.clientId; + static String? getClientId() => getCurrentLogin()?.clientId; - static String getClientSecret() => getCurrentLogin()?.clientSecret; + static String? getClientSecret() => getCurrentLogin()?.clientSecret; - static bool getHasMigrated() => _getPrefBool(KEY_HAS_MIGRATED); + static bool getHasMigrated() => _getPrefBool(KEY_HAS_MIGRATED) ?? false; - static Future setHasMigrated(bool hasMigrated) => _setPrefBool(KEY_HAS_MIGRATED, hasMigrated); + static Future setHasMigrated(bool? hasMigrated) => _setPrefBool(KEY_HAS_MIGRATED, hasMigrated); - static bool getHasCheckedOldReminders() => _getPrefBool(KEY_HAS_CHECKED_OLD_REMINDERS); + static bool getHasCheckedOldReminders() => _getPrefBool(KEY_HAS_CHECKED_OLD_REMINDERS) ?? false; static Future setHasCheckedOldReminders(bool checked) => _setPrefBool(KEY_HAS_CHECKED_OLD_REMINDERS, checked); - static int getCameraCount() => _getPrefInt(KEY_CAMERA_COUNT); + static int? getCameraCount() => _getPrefInt(KEY_CAMERA_COUNT); - static Future setCameraCount(int count) => _setPrefInt(KEY_CAMERA_COUNT, count); + static Future setCameraCount(int? count) => _setPrefInt(KEY_CAMERA_COUNT, count); - static DateTime getRatingNextShowDate() { + static DateTime? getRatingNextShowDate() { final nextShow = _getPrefString(KEY_RATING_NEXT_SHOW_DATE); if (nextShow == null) return null; return DateTime.parse(nextShow); } - static Future setRatingNextShowDate(DateTime nextShowDate) => + static Future setRatingNextShowDate(DateTime? nextShowDate) => _setPrefString(KEY_RATING_NEXT_SHOW_DATE, nextShowDate?.toIso8601String()); - static bool getRatingDontShowAgain() => _getPrefBool(KEY_RATING_DONT_SHOW_AGAIN); + static bool? getRatingDontShowAgain() => _getPrefBool(KEY_RATING_DONT_SHOW_AGAIN); - static Future setRatingDontShowAgain(bool dontShowAgain) => + static Future setRatingDontShowAgain(bool? dontShowAgain) => _setPrefBool(KEY_RATING_DONT_SHOW_AGAIN, dontShowAgain); /// Pref helpers - static Future _setPrefBool(String key, bool value) async { + static Future _setPrefBool(String key, bool? value) async { _checkInit(); - await _prefs.setBool(key, value); + if (value == null) return _prefs!.remove(key); + return _prefs!.setBool(key, value); } - static bool _getPrefBool(String key) { + static bool? _getPrefBool(String key) { _checkInit(); - return _prefs.getBool(key); + return _prefs?.getBool(key); } - static Future _setPrefString(String key, String value) async { + static Future _setPrefString(String key, String? value) async { _checkInit(); - await _prefs.setString(key, value); + if (value == null) return _prefs!.remove(key); + + return _prefs!.setString(key, value); + } + + static String? _getPrefString(String key) { + _checkInit(); + return _prefs?.getString(key); } - static String _getPrefString(String key) { + static int? _getPrefInt(String key) { _checkInit(); - return _prefs.getString(key); + return _prefs?.getInt(key); } - static int _getPrefInt(String key) { + static Future _setPrefInt(String key, int? value) async { _checkInit(); - return _prefs.getInt(key); + if (value == null) return _prefs!.remove(key); + return _prefs!.setInt(key, value); } - static Future _setPrefInt(String key, int value) async { + static Future _removeKey(String key) async { _checkInit(); - return _prefs.setInt(key, value); + return _prefs!.remove(key); } /// Utility functions static Map getHeaderMap({ bool forceDeviceLanguage = false, - String token = null, - Map extraHeaders = null, + String? token = null, + Map? extraHeaders = null, }) { if (token == null) { token = getAuthToken(); @@ -358,9 +371,7 @@ class ApiPrefs { var headers = { 'Authorization': 'Bearer $token', - 'accept-language': (forceDeviceLanguage ? ui.window.locale.toLanguageTag() : effectiveLocale()?.toLanguageTag()) - .replaceAll('-', ',') - .replaceAll('_', '-'), + 'accept-language': (forceDeviceLanguage ? ui.window.locale.toLanguageTag() : effectiveLocale()?.toLanguageTag())?.replaceAll('-', ',').replaceAll('_', '-') ?? '', 'User-Agent': getUserAgent(), }; @@ -371,18 +382,18 @@ class ApiPrefs { return headers; } - static setCurrentStudent(User currentStudent) { + static setCurrentStudent(User? currentStudent) { _checkInit(); if (currentStudent == null) { - _prefs.remove(KEY_CURRENT_STUDENT); + _prefs!.remove(KEY_CURRENT_STUDENT); } else { - _prefs.setString(KEY_CURRENT_STUDENT, json.encode(serialize(currentStudent))); + _prefs!.setString(KEY_CURRENT_STUDENT, json.encode(serialize(currentStudent))); } } - static User getCurrentStudent() { + static User? getCurrentStudent() { _checkInit(); - final studentJson = _prefs.getString(KEY_CURRENT_STUDENT); + final studentJson = _prefs?.getString(KEY_CURRENT_STUDENT); if (studentJson == null || studentJson.isEmpty) return null; return deserialize(json.decode(studentJson)); } diff --git a/apps/flutter_parent/lib/network/utils/authentication_interceptor.dart b/apps/flutter_parent/lib/network/utils/authentication_interceptor.dart index 6059fc90bd..25a83cf5a7 100644 --- a/apps/flutter_parent/lib/network/utils/authentication_interceptor.dart +++ b/apps/flutter_parent/lib/network/utils/authentication_interceptor.dart @@ -36,7 +36,7 @@ class AuthenticationInterceptor extends InterceptorsWrapper { final currentLogin = ApiPrefs.getCurrentLogin(); // Check for any errors - if (error.requestOptions?.path?.contains('accounts/self') == true) { + if (error.requestOptions.path.contains('accounts/self') == true) { // We are likely just checking if the user can masquerade or not, which happens on login - don't try to re-auth here return handler.next(error); } else if (error.requestOptions.headers[_RETRY_HEADER] != null) { @@ -50,23 +50,23 @@ class AuthenticationInterceptor extends InterceptorsWrapper { } // Lock new requests from being processed while refreshing the token - _dio.interceptors?.requestLock?.lock(); - _dio.interceptors?.responseLock?.lock(); + _dio.interceptors.requestLock.lock(); + _dio.interceptors.responseLock.lock(); // Refresh the token and update the login - CanvasToken tokens; + CanvasToken? tokens; tokens = await locator().refreshToken().catchError((e) => null); if (tokens == null) { _logAuthAnalytics(AnalyticsEventConstants.TOKEN_REFRESH_FAILURE_TOKEN_NOT_VALID); - _dio.interceptors?.requestLock?.unlock(); - _dio.interceptors?.responseLock?.unlock(); + _dio.interceptors.requestLock.unlock(); + _dio.interceptors.responseLock.unlock(); return handler.next(error); } else { - Login login = currentLogin.rebuild((b) => b..accessToken = tokens.accessToken); + Login login = currentLogin.rebuild((b) => b..accessToken = tokens?.accessToken); ApiPrefs.addLogin(login); ApiPrefs.switchLogins(login); @@ -76,8 +76,8 @@ class AuthenticationInterceptor extends InterceptorsWrapper { requestOptions.headers['Authorization'] = 'Bearer ${tokens.accessToken}'; requestOptions.headers[_RETRY_HEADER] = _RETRY_HEADER; // Mark retry to prevent infinite recursion - _dio.interceptors?.requestLock?.unlock(); - _dio.interceptors?.responseLock?.unlock(); + _dio.interceptors.requestLock.unlock(); + _dio.interceptors.responseLock.unlock(); final response = await _dio.fetch(requestOptions); if (response.statusCode == 200 || response.statusCode == 201) { @@ -96,4 +96,4 @@ class AuthenticationInterceptor extends InterceptorsWrapper { }; locator().logEvent(eventString, extras: bundle); } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/network/utils/dio_config.dart b/apps/flutter_parent/lib/network/utils/dio_config.dart index 32300c0a21..eaa8e90f1c 100644 --- a/apps/flutter_parent/lib/network/utils/dio_config.dart +++ b/apps/flutter_parent/lib/network/utils/dio_config.dart @@ -16,12 +16,13 @@ import 'dart:io'; import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; -import 'package:dio_http_cache/dio_http_cache.dart'; +import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/network/utils/authentication_interceptor.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; +import 'package:path_provider/path_provider.dart'; import 'private_consts.dart'; @@ -32,7 +33,7 @@ class DioConfig { final Duration cacheMaxAge; final bool forceRefresh; final PageSize pageSize; - final Map extraQueryParams; + final Map? extraQueryParams; final int retries; DioConfig({ @@ -43,21 +44,17 @@ class DioConfig { this.pageSize = PageSize.none, this.extraQueryParams, this.retries = 0, - }) : this.baseHeaders = baseHeaders ?? {}, - assert(baseUrl != null), - assert(cacheMaxAge != null), - assert(forceRefresh != null), - assert(pageSize != null); + }) : this.baseHeaders = baseHeaders ?? {}; /// Creates a copy of this configuration with the given fields replaced with the new values DioConfig copyWith({ - String baseUrl, - Map baseHeaders, - Duration cacheMaxAge, - bool forceRefresh, - PageSize pageSize, - Map extraQueryParams, - int retries, + String? baseUrl, + Map? baseHeaders, + Duration? cacheMaxAge, + bool? forceRefresh, + PageSize? pageSize, + Map? extraQueryParams, + int? retries, }) { return DioConfig( baseUrl: baseUrl ?? this.baseUrl, @@ -87,7 +84,7 @@ class DioConfig { // Add cache configuration to base options if (cacheMaxAge != Duration.zero) { var extras = buildCacheOptions(cacheMaxAge, forceRefresh: forceRefresh).extra; - options.extra.addAll(extras); + if (extras != null) options.extra.addAll(extras); } // Create Dio instance and add interceptors @@ -129,14 +126,13 @@ class DioConfig { // To use proxy add the following run args to run configuration: --dart-define=PROXY={your proxy io}:{proxy port} void _configureDebugProxy(Dio dio) { - const proxy = String.fromEnvironment('PROXY', defaultValue: null); - if (proxy == null) return; + const proxy = String.fromEnvironment('PROXY', defaultValue: ""); + if (proxy == "") return; - (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = - (client) { + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { client.findProxy = (uri) => "PROXY $proxy;"; - client.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; + client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + return client; }; } @@ -151,14 +147,14 @@ class DioConfig { /// Creates a [DioConfig] targeted at typical Canvas API usage static DioConfig canvas({ - bool includeApiPath: true, - bool forceRefresh: false, - bool forceDeviceLanguage: false, - String overrideToken: null, - Map extraHeaders: null, - PageSize pageSize: PageSize.none, + bool includeApiPath = true, + bool forceRefresh = false, + bool forceDeviceLanguage = false, + String? overrideToken = null, + Map? extraHeaders = null, + PageSize pageSize = PageSize.none, }) { - Map extraParams = ApiPrefs.isMasquerading() ? {'as_user_id': ApiPrefs.getUser().id} : null; + Map? extraParams = ApiPrefs.isMasquerading() ? {'as_user_id': ApiPrefs.getUser()?.id} : null; return DioConfig( baseUrl: includeApiPath ? ApiPrefs.getApiUrl() : '${ApiPrefs.getDomain()}/', baseHeaders: ApiPrefs.getHeaderMap( @@ -175,12 +171,12 @@ class DioConfig { /// Creates a [DioConfig] targeted at core/free-for-teacher API usage (i.e. canvas.instructure.com) static DioConfig core({ - bool includeApiPath: true, - Map headers: null, - Duration cacheMaxAge: Duration.zero, - bool forceRefresh: false, - PageSize pageSize: PageSize.none, - bool useBetaDomain: false, + bool includeApiPath = true, + Map? headers = null, + Duration cacheMaxAge = Duration.zero, + bool forceRefresh = false, + PageSize pageSize = PageSize.none, + bool useBetaDomain = false, }) { var baseUrl = useBetaDomain ? 'https://canvas.beta.instructure.com/' : 'https://canvas.instructure.com/'; if (includeApiPath) baseUrl += 'api/v1/'; @@ -203,7 +199,7 @@ class DioConfig { } /// Clears the cache, deleting only the entries related to path OR clearing everything if path is null - Future clearCache({String path}) { + Future clearCache({String? path}) { // The methods below are currently broken in unit tests due to sqflite (even when the sqflite MethodChannel has been // mocked) so we'll just return 'true' for tests. See https://github.com/tekartik/sqflite/issues/83. if (WidgetsBinding.instance.runtimeType != WidgetsFlutterBinding) return Future.value(true); @@ -234,12 +230,12 @@ class PageSize { /// Convenience method that returns a [Dio] instance configured by calling through to [DioConfig.canvas] Dio canvasDio({ - bool includeApiPath: true, - bool forceRefresh: false, - bool forceDeviceLanguage: false, - String overrideToken: null, - Map extraHeaders: null, - PageSize pageSize: PageSize.none, + bool includeApiPath = true, + bool forceRefresh = false, + bool forceDeviceLanguage = false, + String? overrideToken = null, + Map? extraHeaders = null, + PageSize pageSize = PageSize.none, }) { return DioConfig.canvas( forceRefresh: forceRefresh, diff --git a/apps/flutter_parent/lib/network/utils/fetch.dart b/apps/flutter_parent/lib/network/utils/fetch.dart index 528dc2be56..69bb1b102b 100644 --- a/apps/flutter_parent/lib/network/utils/fetch.dart +++ b/apps/flutter_parent/lib/network/utils/fetch.dart @@ -18,7 +18,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/paged_list.dart'; /// Fetches and deserializes a response using the given [request]. -Future fetch(Future> request) async { +Future fetch(Future> request) async { try { final response = await request; return deserialize(response.data); @@ -28,7 +28,7 @@ Future fetch(Future> request) async { } } -Future> fetchFirstPage(Future> request) async { +Future?> fetchFirstPage(Future> request) async { try { final response = await request; return PagedList(response); @@ -38,7 +38,7 @@ Future> fetchFirstPage(Future> request) async } } -Future> fetchNextPage(String nextUrl) async { +Future?> fetchNextPage(String? nextUrl) async { try { var dio = DioConfig.canvas().copyWith(baseUrl: nextUrl).dio; var response = await dio.get(''); @@ -52,18 +52,18 @@ Future> fetchNextPage(String nextUrl) async { /// Fetches and deserializes a list of items using the given [request]. To depaginate the list (i.e. perform multiple /// requests to exhaust pagination), provide a [Dio] instance for [depaginateWith] that is configured for use /// with subsequent page requests (cache behavior, authentications headers, etc). -Future> fetchList( +Future?> fetchList( Future> request, { - Dio depaginateWith: null, + Dio? depaginateWith = null, }) async { try { var response = await request; if (depaginateWith == null) return deserializeList(response.data); - depaginateWith.options.baseUrl = null; - depaginateWith.options.queryParameters?.remove('per_page'); + depaginateWith.options.baseUrl = ''; + depaginateWith.options.queryParameters.remove('per_page'); var pagedList = PagedList(response); while (pagedList.nextUrl != null) { - response = await depaginateWith.get(pagedList.nextUrl); + response = await depaginateWith.get(pagedList.nextUrl ?? ''); pagedList.updateWithResponse(response); } return pagedList.data; diff --git a/apps/flutter_parent/lib/network/utils/paged_list.dart b/apps/flutter_parent/lib/network/utils/paged_list.dart index 9a5a418357..0a84cba8d4 100644 --- a/apps/flutter_parent/lib/network/utils/paged_list.dart +++ b/apps/flutter_parent/lib/network/utils/paged_list.dart @@ -14,10 +14,11 @@ import 'package:dio/dio.dart'; import 'package:flutter_parent/models/serializers.dart'; +import 'package:collection/collection.dart'; /// A helper class to communicate back to api callers what the data and next url is for paged lists. class PagedList { - String nextUrl; + String? nextUrl; List data; PagedList(Response response) @@ -31,12 +32,12 @@ class PagedList { nextUrl = pagedList.nextUrl; } - static String _parseNextUrl(Headers headers) { + static String? _parseNextUrl(Headers? headers) { if (headers == null) return null; - final links = headers['link']?.first?.split(','); - final next = links?.firstWhere((link) => link.contains('rel="next"'), orElse: () => null); + final links = headers['link']?.first.split(','); + final next = links?.firstWhereOrNull((link) => link.contains('rel="next"')); - return next?.substring(1, next?.lastIndexOf('>')); + return next?.substring(1, next.lastIndexOf('>')); } } diff --git a/apps/flutter_parent/lib/parent_app.dart b/apps/flutter_parent/lib/parent_app.dart index 8af1a81639..e7fe9a1e9b 100644 --- a/apps/flutter_parent/lib/parent_app.dart +++ b/apps/flutter_parent/lib/parent_app.dart @@ -34,15 +34,15 @@ class ParentApp extends StatefulWidget { @override _ParentAppState createState() => _ParentAppState(); - ParentApp(this._appCompleter, {Key key}) : super(key: key); + ParentApp(this._appCompleter, {super.key}); - static _ParentAppState of(BuildContext context) { + static _ParentAppState? of(BuildContext context) { return context.findAncestorStateOfType<_ParentAppState>(); } } class _ParentAppState extends State { - Locale _locale; + late Locale? _locale; GlobalKey _navKey = GlobalKey(); rebuild(locale) { @@ -65,7 +65,7 @@ class _ParentAppState extends State { if (!widget._appCompleter.isCompleted) { widget._appCompleter.complete(); } - return MasqueradeUI(navKey: _navKey, child: child); + return MasqueradeUI(navKey: _navKey, child: child!); }, title: 'Canvas Parent', locale: _locale, @@ -89,10 +89,10 @@ class _ParentAppState extends State { // Get notified when there's a new system locale so we can rebuild the app with the new language LocaleResolutionCallback _localeCallback() => (locale, supportedLocales) { // If there is no user locale, they want the system locale. If there is a user locale, we should use it over the system locale - Locale newLocale = ApiPrefs.getUser()?.locale == null ? locale : _locale; + Locale newLocale = ApiPrefs.getUser()?.locale == null ? locale! : _locale!; const fallback = Locale('en'); - Locale resolvedLocale = + Locale? resolvedLocale = AppLocalizations.delegate.resolution(fallback: fallback, matchCountry: false)(newLocale, supportedLocales); // Update the state if the locale changed diff --git a/apps/flutter_parent/lib/router/panda_router.dart b/apps/flutter_parent/lib/router/panda_router.dart index 60f11b245b..cf6f2d3d92 100644 --- a/apps/flutter_parent/lib/router/panda_router.dart +++ b/apps/flutter_parent/lib/router/panda_router.dart @@ -29,13 +29,12 @@ import 'package:flutter_parent/screens/aup/acceptable_use_policy_screen.dart'; import 'package:flutter_parent/screens/calendar/calendar_screen.dart'; import 'package:flutter_parent/screens/calendar/calendar_widget/calendar_widget.dart'; import 'package:flutter_parent/screens/courses/details/course_details_screen.dart'; -import 'package:flutter_parent/screens/courses/details/course_grades_screen.dart'; import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shell_screen.dart'; import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart'; import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart'; import 'package:flutter_parent/screens/events/event_details_screen.dart'; import 'package:flutter_parent/screens/help/help_screen.dart'; -import 'package:flutter_parent/screens/help/legal_screen.dart'; +import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/screens/help/terms_of_use_screen.dart'; import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart'; import 'package:flutter_parent/screens/login_landing_screen.dart'; @@ -117,7 +116,7 @@ class PandaRouter { static String loginWeb( String domain, { String accountName = '', - String authenticationProvider = null, + String? authenticationProvider = null, LoginFlow loginFlow = LoginFlow.normal, }) => '$_loginWeb?${_RouterKeys.domain}=${Uri.encodeQueryComponent(domain)}&${_RouterKeys.accountName}=${Uri.encodeQueryComponent(accountName)}&${_RouterKeys.authenticationProvider}=$authenticationProvider&${_RouterKeys.loginFlow}=${loginFlow.toString()}'; @@ -135,7 +134,7 @@ class PandaRouter { static String _qrPairing = '/qr_pairing'; - static String qrPairing({String pairingUri, bool isCreatingAccount = false}) { + static String qrPairing({String? pairingUri, bool isCreatingAccount = false}) { if (isCreatingAccount) return '$_qrPairing?${_RouterKeys.isCreatingAccount}=${isCreatingAccount}'; if (pairingUri == null) return _qrPairing; return '$_qrPairing?${_RouterKeys.qrPairingInfo}=${Uri.encodeQueryComponent(pairingUri)}'; @@ -160,7 +159,7 @@ class PandaRouter { static String syllabus(String courseId) => '/courses/$courseId/assignments/syllabus'; - static String termsOfUse({String accountId, String domain}) { + static String termsOfUse({String? accountId, String? domain}) { if (accountId != null && domain != null) { return '/terms_of_use?${_RouterKeys.accountId}=${Uri.encodeQueryComponent(accountId)}&${_RouterKeys.url}=${Uri.encodeQueryComponent(domain)}'; } else { @@ -217,27 +216,27 @@ class PandaRouter { // Handlers static Handler _accountCreationHandler = - Handler(handlerFunc: (BuildContext context, Map> params) { + Handler(handlerFunc: (BuildContext? context, Map> params) { var pairingInfo = QRPairingScanResult.success( - params[_RouterKeys.pairingCode][0], params[_RouterKeys.domain][0], params[_RouterKeys.accountId][0]); - return AccountCreationScreen(pairingInfo); + params[_RouterKeys.pairingCode]![0], params[_RouterKeys.domain]![0], params[_RouterKeys.accountId]![0]); + return AccountCreationScreen(pairingInfo as QRPairingInfo); }); - static Handler _alertHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _alertHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return DashboardScreen( startingPage: DashboardContentScreens.Alerts, ); }); static Handler _assignmentDetailsHandler = - Handler(handlerFunc: (BuildContext context, Map> params) { + Handler(handlerFunc: (BuildContext? context, Map> params) { return AssignmentDetailsScreen( - courseId: params[_RouterKeys.courseId][0], - assignmentId: params[_RouterKeys.assignmentId][0], + courseId: params[_RouterKeys.courseId]![0], + assignmentId: params[_RouterKeys.assignmentId]![0], ); }); - static Handler _calendarHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _calendarHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { var calendarParams = { CalendarScreen.startDateKey: DateTime.tryParse(params[_RouterKeys.calendarStart]?.elementAt(0) ?? '') ?? DateTime.now(), @@ -253,103 +252,103 @@ class PandaRouter { return widget; }); - static Handler _conversationsHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _conversationsHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return ConversationListScreen(); }); static Handler _courseAnnouncementDetailsHandler = - Handler(handlerFunc: (BuildContext context, Map> params) { + Handler(handlerFunc: (BuildContext? context, Map> params) { return AnnouncementDetailScreen( - params[_RouterKeys.announcementId][0], AnnouncementType.COURSE, params[_RouterKeys.courseId][0], context); + params[_RouterKeys.announcementId]![0], AnnouncementType.COURSE, params[_RouterKeys.courseId]![0], context); }); - static Handler _courseDetailsHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - return CourseDetailsScreen(params[_RouterKeys.courseId][0]); + static Handler _courseDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + return CourseDetailsScreen(params[_RouterKeys.courseId]![0]); }); - static Handler _coursesHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _coursesHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return DashboardScreen( startingPage: DashboardContentScreens.Courses, ); }); - static Handler _dashboardHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _dashboardHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return DashboardScreen(); }); - static Handler _domainSearchHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _domainSearchHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { // Login flow - String loginFlowString = params[_RouterKeys.loginFlow].elementAt(0) ?? LoginFlow.normal.toString(); + String loginFlowString = params[_RouterKeys.loginFlow]?.elementAt(0) ?? LoginFlow.normal.toString(); LoginFlow loginFlow = LoginFlow.values.firstWhere((e) => e.toString() == loginFlowString); return DomainSearchScreen(loginFlow: loginFlow); }); - static Handler _eventDetailsHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _eventDetailsHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return EventDetailsScreen.withId( - eventId: params[_RouterKeys.eventId][0], - courseId: params[_RouterKeys.courseId][0], + eventId: params[_RouterKeys.eventId]![0], + courseId: params[_RouterKeys.courseId]![0], ); }); - static Handler _frontPageHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - return CourseRoutingShellScreen(params[_RouterKeys.courseId][0], CourseShellType.frontPage); + static Handler _frontPageHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + return CourseRoutingShellScreen(params[_RouterKeys.courseId]![0], CourseShellType.frontPage); }); - static Handler _gradesPageHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - return CourseDetailsScreen(params[_RouterKeys.courseId][0]); + static Handler _gradesPageHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + return CourseDetailsScreen(params[_RouterKeys.courseId]![0]); }); - static Handler _helpHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _helpHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return HelpScreen(); }); static Handler _institutionAnnouncementDetailsHandler = - Handler(handlerFunc: (BuildContext context, Map> params) { + Handler(handlerFunc: (BuildContext? context, Map> params) { return AnnouncementDetailScreen( - params[_RouterKeys.accountNotificationId][0], AnnouncementType.INSTITUTION, '', context); + params[_RouterKeys.accountNotificationId]![0], AnnouncementType.INSTITUTION, '', context); }); - static Handler _legalHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _legalHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return LegalScreen(); }); - static Handler _loginHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _loginHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return LoginLandingScreen(); }); - static Handler _loginWebHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _loginWebHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { // Auth provider - String authProvider = params[_RouterKeys.authenticationProvider]?.elementAt(0); + String? authProvider = params[_RouterKeys.authenticationProvider]?.elementAt(0); if ('null' == authProvider) authProvider = null; // Login flow - String loginFlowString = params[_RouterKeys.loginFlow].elementAt(0); + String? loginFlowString = params[_RouterKeys.loginFlow]?.elementAt(0); if (loginFlowString == null || loginFlowString == 'null') loginFlowString = LoginFlow.normal.toString(); LoginFlow loginFlow = LoginFlow.values.firstWhere((e) => e.toString() == loginFlowString); return WebLoginScreen( - params[_RouterKeys.domain][0], - accountName: params[_RouterKeys.accountName][0], + params[_RouterKeys.domain]![0], + accountName: params[_RouterKeys.accountName]![0], authenticationProvider: authProvider, loginFlow: loginFlow, ); }); - static Handler _notParentHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _notParentHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return NotAParentScreen(); }); - static Handler _qrLoginHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - String qrLoginUrl = params[_RouterKeys.qrLoginUrl]?.elementAt(0); + static Handler _qrLoginHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + String? qrLoginUrl = params[_RouterKeys.qrLoginUrl]?.elementAt(0); return SplashScreen(qrLoginUrl: qrLoginUrl); }); - static Handler _qrTutorialHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _qrTutorialHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return QRLoginTutorialScreen(); }); - static Handler _qrPairingHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _qrPairingHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { var pairingInfo = QRUtils.parsePairingInfo(params[_RouterKeys.qrPairingInfo]?.elementAt(0)); var isCreatingAccount = params[_RouterKeys.isCreatingAccount]?.elementAt(0) == 'true'; if (pairingInfo is QRPairingInfo) { @@ -361,30 +360,30 @@ class PandaRouter { } }); - static Handler _rootSplashHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _rootSplashHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return SplashScreen(); }); - static Handler _routerErrorHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - final url = params[_RouterKeys.url][0]; + static Handler _routerErrorHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + final url = params[_RouterKeys.url]![0]; return RouterErrorScreen(url); }); - static Handler _settingsHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _settingsHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return SettingsScreen(); }); - static Handler _simpleWebViewHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - final url = params[_RouterKeys.url][0]; + static Handler _simpleWebViewHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + final url = params[_RouterKeys.url]![0]; final infoText = params[_RouterKeys.infoText]?.elementAt(0); return SimpleWebViewScreen(url, url, infoText: infoText == null || infoText == 'null' ? null : infoText); }); - static Handler _syllabusHandler = Handler(handlerFunc: (BuildContext context, Map> params) { - return CourseRoutingShellScreen(params[_RouterKeys.courseId][0], CourseShellType.syllabus); + static Handler _syllabusHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { + return CourseRoutingShellScreen(params[_RouterKeys.courseId]![0], CourseShellType.syllabus); }); - static Handler _termsOfUseHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _termsOfUseHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { final domain = params[_RouterKeys.url]?.elementAt(0); final accountId = params[_RouterKeys.accountId]?.elementAt(0); @@ -395,14 +394,14 @@ class PandaRouter { } }); - static Handler _aupHandler = Handler(handlerFunc: (BuildContext context, Map> params) { + static Handler _aupHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { return AcceptableUsePolicyScreen(); }); /// Used to handled external urls routed by the intent-filter -> MainActivity.kt static Handler _rootWithExternalUrlHandler = - Handler(handlerFunc: (BuildContext context, Map> params) { - var link = params[_RouterKeys.url][0]; + Handler(handlerFunc: (BuildContext? context, Map> params) { + var link = params[_RouterKeys.url]![0]; // QR Login: we need to modify the url slightly var qrUri = QRUtils.verifySSOLogin(link); @@ -412,7 +411,7 @@ class PandaRouter { // QR Pairing var pairingParseResult = QRUtils.parsePairingInfo(link); - QRPairingInfo pairingInfo = pairingParseResult is QRPairingInfo ? pairingParseResult : null; + QRPairingInfo? pairingInfo = pairingParseResult is QRPairingInfo ? pairingParseResult : null; if (pairingInfo != null) { link = qrPairing(pairingUri: link); } @@ -427,8 +426,8 @@ class PandaRouter { // Before deep linking, we need to make sure a current student is set if (ApiPrefs.getCurrentStudent() != null || qrUri != null) { // If its a link we can handle natively and within our domain, route - return (urlRouteWrapper.appRouteMatch.route.handler as Handler) - .handlerFunc(context, urlRouteWrapper.appRouteMatch.parameters); + return (urlRouteWrapper.appRouteMatch?.route.handler as Handler) + .handlerFunc(context, urlRouteWrapper.appRouteMatch!.parameters); } else { // This might be a migrated user or an error case, let's route them to the dashboard return _dashboardHandler.handlerFunc(context, {}); @@ -534,7 +533,7 @@ class _RouterKeys { class _UrlRouteWrapper { final String path; final bool validHost; - final AppRouteMatch appRouteMatch; + final AppRouteMatch? appRouteMatch; _UrlRouteWrapper(this.path, this.validHost, this.appRouteMatch); } diff --git a/apps/flutter_parent/lib/router/router_error_screen.dart b/apps/flutter_parent/lib/router/router_error_screen.dart index b132a1cbf1..ca05fa58a2 100644 --- a/apps/flutter_parent/lib/router/router_error_screen.dart +++ b/apps/flutter_parent/lib/router/router_error_screen.dart @@ -52,35 +52,39 @@ class RouterErrorScreen extends StatelessWidget { child: Text( L10n(context).routerErrorMessage, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16), + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16), ), ), Padding( padding: const EdgeInsets.fromLTRB(48, 0, 48, 0), - child: FlatButton( + child: TextButton( onPressed: () { locator().launch(_route); }, child: Text(L10n(context).openInBrowser, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16)), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(4.0), - side: BorderSide(color: ParentColors.tiara), + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16)), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(4.0), + side: BorderSide(color: ParentColors.tiara), + ), ), ), ), SizedBox(height: 28), Padding( padding: const EdgeInsets.fromLTRB(48, 0, 48, 0), - child: FlatButton( + child: TextButton( onPressed: () { _switchUsers(context); }, child: - Text(L10n(context).switchUsers, style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16)), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(4.0), - side: BorderSide(color: ParentColors.tiara), + Text(L10n(context).switchUsers, style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16)), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(4.0), + side: BorderSide(color: ParentColors.tiara), + ), ), ), ), @@ -88,10 +92,10 @@ class RouterErrorScreen extends StatelessWidget { ); } - _switchUsers(BuildContext context) async { - await ParentTheme.of(context).setSelectedStudent(null); // TODO - Test this, do we need it here? + Future _switchUsers(BuildContext context) async { + await ParentTheme.of(context)?.setSelectedStudent(null); // TODO - Test this, do we need it here? await ApiPrefs.performLogout(switchingLogins: true, app: ParentApp.of(context)); await FeaturesUtils.performLogout(); - locator().pushRouteAndClearStack(context, PandaRouter.login()); + await locator().pushRouteAndClearStack(context, PandaRouter.login()); } } diff --git a/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart b/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart index 17026e6b27..584172d22a 100644 --- a/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart +++ b/apps/flutter_parent/lib/screens/account_creation/account_creation_interactor.dart @@ -21,7 +21,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; class AccountCreationInteractor { - Future getToSForAccount(String accountId, String domain) { + Future getToSForAccount(String accountId, String domain) { return locator().getTermsOfServiceForAccount(accountId, domain); } diff --git a/apps/flutter_parent/lib/screens/account_creation/account_creation_screen.dart b/apps/flutter_parent/lib/screens/account_creation/account_creation_screen.dart index 3736d479c9..b4111ae745 100644 --- a/apps/flutter_parent/lib/screens/account_creation/account_creation_screen.dart +++ b/apps/flutter_parent/lib/screens/account_creation/account_creation_screen.dart @@ -39,7 +39,7 @@ class AccountCreationScreen extends StatefulWidget { final QRPairingInfo pairingInfo; - const AccountCreationScreen(this.pairingInfo, {Key key}) : super(key: key); + const AccountCreationScreen(this.pairingInfo, {super.key}); @override _AccountCreationScreenState createState() => _AccountCreationScreenState(); @@ -49,9 +49,9 @@ class _AccountCreationScreenState extends State { TextStyle _defaultSpanStyle = TextStyle(color: ParentColors.ash, fontSize: 14.0, fontWeight: FontWeight.normal); TextStyle _linkSpanStyle = TextStyle(color: ParentColors.parentApp, fontSize: 14.0, fontWeight: FontWeight.normal); - Future _tosFuture; + Future? _tosFuture; - Future _getToS() { + Future _getToS() { return locator() .getToSForAccount(widget.pairingInfo.accountId, widget.pairingInfo.domain); } @@ -61,7 +61,7 @@ class _AccountCreationScreenState extends State { final FocusNode _emailFocus = FocusNode(); final _emailController = TextEditingController(); - String _emailErrorText = null; + String? _emailErrorText = null; final FocusNode _passwordFocus = FocusNode(); final _passwordController = TextEditingController(); @@ -152,7 +152,7 @@ class _AccountCreationScreenState extends State { _fieldFocusChange(_nameFocus, _emailFocus); }, validator: (value) { - if (value.isEmpty) { + if (value == null || value.isEmpty) { return L10n(context).qrCreateAccountNameError; } else { return null; @@ -187,10 +187,10 @@ class _AccountCreationScreenState extends State { validator: (value) => _validateEmail(value)); } - String _validateEmail(String value, {bool apiError = false}) { + String? _validateEmail(String? value, {bool apiError = false}) { if (apiError) { return L10n(context).qrCreateAccountInvalidEmailError; - } else if (value.isEmpty) { + } else if (value == null || value.isEmpty) { return L10n(context).qrCreateAccountEmailError; } else if (!EmailValidator.validate(value)) { return L10n(context).qrCreateAccountInvalidEmailError; @@ -233,10 +233,10 @@ class _AccountCreationScreenState extends State { )), onFieldSubmitted: (term) { _clearFieldFocus(); - _formKey.currentState.validate(); + _formKey.currentState?.validate(); }, validator: (value) { - if (value.isEmpty) { + if (value == null || value.isEmpty) { return L10n(context).qrCreateAccountPasswordError; } else if (value.length < 8) { return L10n(context).qrCreateAccountPasswordLengthError; @@ -275,7 +275,7 @@ class _AccountCreationScreenState extends State { Widget _createAccountTOS() { return FutureBuilder( future: _tosFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container(color: Theme.of(context).scaffoldBackgroundColor, child: LoadingIndicator()); } @@ -284,7 +284,7 @@ class _AccountCreationScreenState extends State { return _getTosSpan(null); } else { var terms = snapshot.data; - if (terms.passive) { + if (terms == null || terms.passive) { return _getPrivacyPolicySpan(); } else { return _getTosSpan(snapshot.data); @@ -307,8 +307,8 @@ class _AccountCreationScreenState extends State { ); } - TextSpan _getTosSpanHelper({@required String text, @required List inputSpans}) { - var indexedSpans = inputSpans.map((it) => MapEntry(text.indexOf(it.text), it)).toList(); + TextSpan _getTosSpanHelper({required String text, required List inputSpans}) { + var indexedSpans = inputSpans.map((it) => MapEntry(text.indexOf(it.text!), it)).toList(); indexedSpans.sort((a, b) => a.key.compareTo(b.key)); int index = 0; @@ -317,14 +317,14 @@ class _AccountCreationScreenState extends State { for (var indexedSpan in indexedSpans) { spans.add(TextSpan(text: text.substring(index, indexedSpan.key))); spans.add(indexedSpan.value); - index = indexedSpan.key + indexedSpan.value.text.length; + index = indexedSpan.key + indexedSpan.value.text!.length; } spans.add(TextSpan(text: text.substring(index))); return TextSpan(children: spans); } - Widget _getTosSpan(TermsOfService terms) { + Widget _getTosSpan(TermsOfService? terms) { var termsOfService = L10n(context).qrCreateAccountTermsOfService; var privacyPolicy = L10n(context).qrCreateAccountPrivacyPolicy; var body = L10n(context).qrCreateAccountTos(termsOfService, privacyPolicy); @@ -359,19 +359,21 @@ class _AccountCreationScreenState extends State { Widget _createAccountButton() { return ButtonTheme( - child: RaisedButton( + child: ElevatedButton( child: Padding( padding: const EdgeInsets.all(16.0), child: _isLoading ? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Colors.white))) : Text( L10n(context).qrCreateAccount, - style: TextStyle(fontSize: 16), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontSize: 16, color: Colors.white), ), ), - color: Theme.of(context).accentColor, - textColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))), + ), onPressed: () { if (!_isLoading) _handleCreateAccount(); }, @@ -385,14 +387,17 @@ class _AccountCreationScreenState extends State { onTap: () => locator().pushRoute(context, PandaRouter.loginWeb(widget.pairingInfo.domain)), child: Padding( padding: const EdgeInsets.fromLTRB(0, 14, 0, 14), - child: Center( - child: RichText( - text: TextSpan( - style: _defaultSpanStyle, - children: [ - TextSpan(text: L10n(context).qrCreateAccountSignIn1), - TextSpan(text: L10n(context).qrCreateAccountSignIn2, style: _linkSpanStyle) - ], + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: 48), + child: Center( + child: RichText( + text: TextSpan( + style: _defaultSpanStyle, + children: [ + TextSpan(text: L10n(context).qrCreateAccountSignIn1), + TextSpan(text: L10n(context).qrCreateAccountSignIn2, style: _linkSpanStyle) + ], + ), ), ), ), @@ -401,7 +406,7 @@ class _AccountCreationScreenState extends State { } void _handleCreateAccount() async { - if (_formKey.currentState.validate()) { + if (_formKey.currentState?.validate() == true) { setState(() => _isLoading = true); try { var response = await locator().createNewAccount( @@ -434,7 +439,7 @@ class _AccountCreationScreenState extends State { if (e is DioError) { _handleDioError(e); } else { - _scaffoldKey.currentState.showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n(context).unexpectedError)), ); } @@ -446,7 +451,7 @@ class _AccountCreationScreenState extends State { String emailError = ''; String pairingError = ''; try { - emailError = e.response.data['errors']['user']['pseudonyms'][0]['message']; + emailError = e.response?.data['errors']['user']['pseudonyms'][0]['message']; if (emailError.isNotEmpty) { setState(() { _emailErrorText = _validateEmail('', apiError: true); @@ -457,9 +462,9 @@ class _AccountCreationScreenState extends State { } try { - pairingError = e.response.data['errors']['pairing_code']['code'][0]['message']; + pairingError = e.response?.data['errors']['pairing_code']['code'][0]['message']; if (pairingError.isNotEmpty) { - _scaffoldKey.currentState.showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n(context).errorPairingFailed)), ); } @@ -469,7 +474,7 @@ class _AccountCreationScreenState extends State { if (pairingError.isEmpty && emailError.isEmpty) { // Show generic error case - _scaffoldKey.currentState.showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n(context).errorGenericPairingFailed)), ); } diff --git a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart index 30099e0097..6fb99e039f 100644 --- a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart +++ b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_extensions.dart @@ -49,20 +49,20 @@ extension GetTitleFromAlert on AlertType { } } -extension GetThresholdFromType on List { - AlertThreshold getThreshold(AlertType type) { - var index = this.indexWhere((threshold) => threshold?.alertType == type); - if (index == -1) +extension GetThresholdFromType on List? { + AlertThreshold? getThreshold(AlertType type) { + var index = this?.indexWhere((threshold) => threshold?.alertType == type); + if (index == null || index == -1) return null; else - return this[index]; + return this?[index]; } } extension GetThresholdMinMax on AlertType { - List getMinMax(List thresholds) { - String max; - String min; + List getMinMax(List? thresholds) { + String? max; + String? min; if (this == AlertType.courseGradeLow) { max = thresholds.getThreshold(AlertType.courseGradeHigh)?.threshold; diff --git a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_interactor.dart b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_interactor.dart index 92a2c99eeb..40c544d385 100644 --- a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_interactor.dart +++ b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_interactor.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/network/api/enrollments_api.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class AlertThresholdsInteractor { - Future> getAlertThresholdsForStudent(String studentId, {bool forceRefresh = true}) async { + Future?> getAlertThresholdsForStudent(String studentId, {bool forceRefresh = true}) async { return locator().getAlertThresholds(studentId, forceRefresh); } @@ -31,8 +31,8 @@ class AlertThresholdsInteractor { /// [value] is only used when creating percentages /// /// - Future updateAlertThreshold(AlertType type, String studentId, AlertThreshold threshold, - {String value}) { + Future updateAlertThreshold(AlertType type, String studentId, AlertThreshold? threshold, + {String? value}) { var api = locator(); if (type.isSwitch()) { if (threshold == null) { @@ -49,7 +49,7 @@ class AlertThresholdsInteractor { return api.createThreshold(type, studentId, value: value); } else // Disable the threshold - return api.deleteAlert(threshold); + return api.deleteAlert(threshold!); } } diff --git a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_percentage_dialog.dart b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_percentage_dialog.dart index 83d620cca4..6ca788ee4b 100644 --- a/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_percentage_dialog.dart +++ b/apps/flutter_parent/lib/screens/alert_thresholds/alert_thresholds_percentage_dialog.dart @@ -26,7 +26,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; class AlertThresholdsPercentageDialog extends StatefulWidget { final AlertType _alertType; - final List thresholds; + final List? thresholds; final String _studentId; AlertThresholdsPercentageDialog(this.thresholds, this._alertType, this._studentId); @@ -37,18 +37,18 @@ class AlertThresholdsPercentageDialog extends StatefulWidget { class AlertThresholdsPercentageDialogState extends State { bool _disableButtons = false; - AlertThreshold _threshold; + AlertThreshold? _threshold; bool _networkError = false; bool _neverClicked = false; - String maxValue; - String minValue; + String? maxValue; + String? minValue; final int _disabledAlpha = 80; static final UniqueKey okButtonKey = UniqueKey(); // For testing - String errorMsg; + String? errorMsg; final GlobalKey _formKey = GlobalKey(); @@ -77,37 +77,40 @@ class AlertThresholdsPercentageDialogState extends State ArrowAwareFocusScope( node: _focusScopeNode, child: AlertDialog( + scrollable: true, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), title: Text(widget._alertType.getTitle(context)), content: TextFormField( key: _formKey, autofocus: true, - autovalidate: true, + autovalidateMode: AutovalidateMode.always, keyboardType: TextInputType.number, initialValue: _threshold?.threshold, maxLength: 3, - buildCounter: (_, {currentLength, maxLength, isFocused}) => null, // Don't show the counter + buildCounter: (_, {required currentLength, maxLength, required isFocused}) => null, // Don't show the counter inputFormatters: [ // Only accept numbers, no other characters (including '-') - BlacklistingTextInputFormatter(RegExp('[^0-9]')), + FilteringTextInputFormatter.deny(RegExp('[^0-9]')), ], onChanged: (input) { errorMsg = null; // Check if we had a network error - if (input == null || input.isEmpty) { + if (input.isEmpty) { // Don't validate when there's no input (no error) errorMsg = null; } else { var inputParsed = int.tryParse(input); - var maxParsed = maxValue != null ? int.tryParse(maxValue) : 100; - var minParsed = minValue != null ? int.tryParse(minValue) : null; - - if (maxParsed == 100 && inputParsed > 100) { - errorMsg = L10n(context).mustBeBelow100; - } else if (maxParsed != 100 && inputParsed >= maxParsed) { - errorMsg = L10n(context).mustBeBelowN(maxParsed); - } else if (minParsed != null && inputParsed <= minParsed) { - errorMsg = L10n(context).mustBeAboveN(minParsed); + var maxParsed = maxValue != null ? int.tryParse(maxValue!) ?? 100 : 100; + var minParsed = minValue != null ? int.tryParse(minValue!) ?? 0 : 0; + + if (inputParsed != null) { + if (maxParsed == 100 && inputParsed > 100) { + errorMsg = L10n(context).mustBeBelow100; + } else if (maxParsed != 100 && inputParsed >= maxParsed) { + errorMsg = L10n(context).mustBeBelowN(maxParsed); + } else if (inputParsed <= minParsed) { + errorMsg = L10n(context).mustBeAboveN(minParsed); + } } } @@ -128,9 +131,9 @@ class AlertThresholdsPercentageDialogState extends State() .updateAlertThreshold(widget._alertType, widget._studentId, _threshold, - value: input.isNotEmpty && !_neverClicked ? input : '-1') + value: ((input == null || input.isNotEmpty) && !_neverClicked) ? input : '-1') .catchError((_) => null); if (result != null) { // Threshold was updated/deleted successfully - if (input.isEmpty || _neverClicked) { + if (input == null || input.isEmpty || _neverClicked) { // Deleted a threshold - Navigator.of(context).pop(_threshold.rebuild((b) => b.threshold = '-1')); + Navigator.of(context).pop(_threshold?.rebuild((b) => b.threshold = '-1')); } else { // Updated a threshold Navigator.of(context).pop(result); @@ -159,7 +162,7 @@ class AlertThresholdsPercentageDialogState extends State[ - FlatButton( + TextButton( child: Text(L10n(context).cancel.toUpperCase()), - disabledTextColor: ParentColors.parentApp.withAlpha(_disabledAlpha), + style: TextButton.styleFrom(disabledBackgroundColor: ParentColors.parentApp.withAlpha(_disabledAlpha)), onPressed: () { Navigator.of(context).pop(null); }), - FlatButton( + TextButton( child: Text(L10n(context).never.toUpperCase()), - disabledTextColor: ParentColors.parentApp.withAlpha(_disabledAlpha), + style: TextButton.styleFrom(disabledBackgroundColor: ParentColors.parentApp.withAlpha(_disabledAlpha)), onPressed: () async { if (_threshold == null) { // Threshold is already disabled @@ -186,17 +189,17 @@ class AlertThresholdsPercentageDialogState extends State b.threshold = '-1'); _neverClicked = true; _showNetworkError(false); - _formKey.currentState.save(); + _formKey.currentState?.save(); }), - FlatButton( + TextButton( key: okButtonKey, child: Text(L10n(context).ok), - disabledTextColor: ParentColors.parentApp.withAlpha(_disabledAlpha), + style: TextButton.styleFrom(disabledBackgroundColor: ParentColors.parentApp.withAlpha(_disabledAlpha)), onPressed: _disableButtons ? null : () async { _showNetworkError(false); - _formKey.currentState.save(); + _formKey.currentState?.save(); }, ), ], @@ -206,7 +209,7 @@ class AlertThresholdsPercentageDialogState extends State { - Future> _thresholdsFuture; - Future _canDeleteStudentFuture; + late Future?> _thresholdsFuture; + late Future _canDeleteStudentFuture; - Future> _loadThresholds() => + Future?> _loadThresholds() => locator().getAlertThresholdsForStudent(widget._student.id); - List _thresholds = []; + List? _thresholds = []; @override void initState() { @@ -62,19 +62,21 @@ class AlertThresholdsState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(L10n(context).alertSettings), - bottom: ParentTheme.of(context).appBarDivider(), + bottom: ParentTheme.of(context)?.appBarDivider(), actions: [_deleteOption()], ), body: FutureBuilder( future: _thresholdsFuture, - builder: (context, AsyncSnapshot> snapshot) { + builder: (context, AsyncSnapshot?> snapshot) { Widget view; if (snapshot.hasError) { view = _error(context); } else if (snapshot.connectionState == ConnectionState.waiting) { view = LoadingIndicator(); } else { - _thresholds = snapshot.data; + if (snapshot.hasData) { + _thresholds = snapshot.data!; + } view = _body(); } return view; @@ -120,20 +122,19 @@ class AlertThresholdsState extends State { padding: const EdgeInsets.only(top: 16), child: Text( L10n(context).deleteStudentFailure, - style: Theme.of(context).textTheme.bodyText1.copyWith(color: ParentColors.failure), + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: ParentColors.failure), ), ) ], ), actions: [ if (!busy) - FlatButton( + TextButton( child: Text(L10n(context).cancel.toUpperCase()), - onPressed: () => Navigator.of(context).pop(), - textColor: ParentColors.ash, + onPressed: () => Navigator.of(context).pop() ), if (!busy) - FlatButton( + TextButton( child: Text(L10n(context).delete.toUpperCase()), onPressed: () async { setState(() { @@ -180,7 +181,7 @@ class AlertThresholdsState extends State { ), title: UserName.fromUser( widget._student, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, ), ), SizedBox( @@ -195,7 +196,7 @@ class AlertThresholdsState extends State { ), child: Text( L10n(context).alertMeWhen, - style: Theme.of(context).textTheme.subtitle1.copyWith(color: ParentColors.ash), + style: Theme.of(context).textTheme.bodyMedium, ), )), Expanded( @@ -232,18 +233,18 @@ class AlertThresholdsState extends State { Widget _generateAlertThresholdTile(AlertType type) => type.isPercentage() ? _percentageTile(type) : _switchTile(type); Widget _percentageTile(AlertType type) { - int value = int.tryParse(_thresholds.getThreshold(type)?.threshold ?? ''); + int? value = int.tryParse(_thresholds?.getThreshold(type)?.threshold ?? ''); return ListTile( title: Text( type.getTitle(context), - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, ), trailing: Text( value != null ? NumberFormat.percentPattern().format(value / 100) : L10n(context).never, - style: Theme.of(context).textTheme.subtitle1.copyWith(color: StudentColorSet.electric.light), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: StudentColorSet.electric.light), ), onTap: () async { - AlertThreshold update = await showDialog( + AlertThreshold? update = await showDialog( context: context, builder: (context) => AlertThresholdsPercentageDialog(_thresholds, type, widget._student.id)); if (update == null) { @@ -252,24 +253,24 @@ class AlertThresholdsState extends State { } // Grab the index of the threshold, if it exists - var idx = _thresholds.indexWhere((threshold) => threshold?.alertType == type); + var idx = _thresholds?.indexWhere((threshold) => threshold?.alertType == type); // Update the UI setState(() { if (update.threshold != '-1') { // Threshold was created or updated - if (idx == -1) { + if (idx == null || idx == -1) { // Threshold got created - _thresholds.add(update); + _thresholds?.add(update); } else { // Existing threshold was updated - _thresholds[idx] = update; + _thresholds?[idx] = update; } } else { // Threshold was either deleted or left at 'never' - if (idx != -1) { + if (idx != null && idx != -1) { // Threshold exists but was deleted - _thresholds[idx] = null; + _thresholds?.removeAt(idx); } } }); @@ -278,7 +279,7 @@ class AlertThresholdsState extends State { } Widget _switchTile(AlertType type) { - AlertThreshold threshold = _thresholds.getThreshold(type); + AlertThreshold? threshold = _thresholds?.getThreshold(type); bool value = threshold != null; return _TalkbackSwitchTile( title: type.getTitle(context), @@ -289,18 +290,18 @@ class AlertThresholdsState extends State { ); } - Future _updateThreshold(AlertType type, AlertThreshold threshold) async { + Future _updateThreshold(AlertType type, AlertThreshold? threshold) async { var update = await locator().updateAlertThreshold(type, widget._student.id, threshold); // Grab the index of the threshold, if it exists - var idx = _thresholds.indexWhere((t) => t?.alertType == type); + var idx = _thresholds?.indexWhere((t) => t?.alertType == type); setState(() { - if (idx == -1) { + if (idx == null || idx == -1) { // Threshold got created - _thresholds.add(update); + _thresholds?.add(update); } else { // Existing threshold was deleted - _thresholds[idx] = null; + _thresholds?[idx] = null; } }); } @@ -310,17 +311,17 @@ class AlertThresholdsState extends State { /// update the value too late for talkback, so it reads the previous value. class _TalkbackSwitchTile extends StatefulWidget { final String title; - final bool initValue; + final bool? initValue; final ValueChanged onChange; - const _TalkbackSwitchTile({Key key, this.title, this.initValue, this.onChange}) : super(key: key); + const _TalkbackSwitchTile({required this.title, this.initValue, required this.onChange, super.key}); @override _TalkbackSwitchTileState createState() => _TalkbackSwitchTileState(); } class _TalkbackSwitchTileState extends State<_TalkbackSwitchTile> { - bool _value; + late bool _value; @override void initState() { @@ -331,7 +332,7 @@ class _TalkbackSwitchTileState extends State<_TalkbackSwitchTile> { @override Widget build(BuildContext context) { return SwitchListTile( - title: Text(widget.title), + title: Text(widget.title, style: Theme.of(context).textTheme.titleMedium), value: _value, contentPadding: const EdgeInsets.fromLTRB(16, 0, 7, 0), onChanged: (bool state) { diff --git a/apps/flutter_parent/lib/screens/alerts/alerts_interactor.dart b/apps/flutter_parent/lib/screens/alerts/alerts_interactor.dart index be97b34e04..303a2a0b8c 100644 --- a/apps/flutter_parent/lib/screens/alerts/alerts_interactor.dart +++ b/apps/flutter_parent/lib/screens/alerts/alerts_interactor.dart @@ -20,15 +20,15 @@ import 'package:flutter_parent/utils/alert_helper.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class AlertsInteractor { - Future getAlertsForStudent(String studentId, bool forceRefresh) async { - final alertsFuture = _alertsApi().getAlertsDepaginated(studentId, forceRefresh)?.then((List list) async { + Future getAlertsForStudent(String studentId, bool forceRefresh) async { + final alertsFuture = _alertsApi().getAlertsDepaginated(studentId, forceRefresh)?.then((List? list) async { return locator().filterAlerts(list); - })?.then((list) => list - ..sort((a, b) { + }).then((list) => list + ?..sort((a, b) { if (a.actionDate == null && b.actionDate == null) return 0; if (a.actionDate == null && b.actionDate != null) return -1; if (a.actionDate != null && b.actionDate == null) return 1; - return b.actionDate.compareTo(a.actionDate); + return b.actionDate!.compareTo(a.actionDate!); })); final thresholdsFuture = _alertsApi().getAlertThresholds(studentId, forceRefresh); @@ -38,7 +38,7 @@ class AlertsInteractor { return AlertsList(await alertsFuture, await thresholdsFuture); } - Future markAlertRead(String studentId, String alertId) { + Future markAlertRead(String studentId, String alertId) { return _alertsApi().updateAlertWorkflow(studentId, alertId, AlertWorkflowState.read.name); } @@ -50,8 +50,8 @@ class AlertsInteractor { } class AlertsList { - final List alerts; - final List thresholds; + final List? alerts; + final List? thresholds; AlertsList(this.alerts, this.thresholds); } diff --git a/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart b/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart index c2186f0436..d9ae32d731 100644 --- a/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart +++ b/apps/flutter_parent/lib/screens/alerts/alerts_screen.dart @@ -41,10 +41,10 @@ class AlertsScreen extends StatefulWidget { } class _AlertsScreenState extends State { - Future _alertsFuture; - User _student; + Future? _alertsFuture; + late User _student; - Future _loadAlerts({bool forceRefresh = false}) => + Future _loadAlerts({bool forceRefresh = false}) => widget._interactor.getAlertsForStudent(_student.id, forceRefresh); GlobalKey _refreshKey = GlobalKey(); @@ -52,7 +52,7 @@ class _AlertsScreenState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - var _selectedStudent = Provider.of(context, listen: true).value; + var _selectedStudent = Provider.of(context, listen: true).value!; if (_alertsFuture == null) { // First time _student = _selectedStudent; @@ -71,7 +71,7 @@ class _AlertsScreenState extends State { return FutureBuilder( key: _refreshKey, future: _alertsFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { // Show loading if we're waiting for data, not inside the refresh indicator as it's unnecessary if (snapshot.connectionState == ConnectionState.waiting) { return LoadingIndicator(); @@ -86,10 +86,10 @@ class _AlertsScreenState extends State { } return RefreshIndicator( - onRefresh: () { + onRefresh: () async { _alertsFuture = _loadAlerts(forceRefresh: true); setState(() {}); - return _alertsFuture; + await _alertsFuture; }, child: child, ); @@ -105,10 +105,10 @@ class _AlertsScreenState extends State { /// A helper widget to handle updating read status of alerts, and displaying as a list class _AlertsList extends StatefulWidget { final _interactor = locator(); - final AlertsList _data; + final AlertsList? _data; final User _student; - _AlertsList(this._student, this._data, {Key key}) : super(key: key); + _AlertsList(this._student, this._data, {super.key}); @override __AlertsListState createState() => __AlertsListState(); @@ -116,7 +116,7 @@ class _AlertsList extends StatefulWidget { class __AlertsListState extends State<_AlertsList> { GlobalKey _listKey = GlobalKey(); - AlertsList _data; + AlertsList? _data; @override void initState() { @@ -126,18 +126,18 @@ class __AlertsListState extends State<_AlertsList> { @override Widget build(BuildContext context) { - if (_data == null || _data.alerts == null || _data.alerts.isEmpty) { + if (_data == null || _data?.alerts == null || _data?.alerts?.isEmpty == true) { return _empty(context); } else { return AnimatedList( key: _listKey, - initialItemCount: _data.alerts.length, - itemBuilder: (context, index, animation) => _alertTile(context, _data.alerts[index], index), + initialItemCount: _data!.alerts!.length, + itemBuilder: (context, index, animation) => _alertTile(context, _data!.alerts![index], index), ); } } - Widget _alertTile(BuildContext context, Alert alert, int index, {Animation animation = null}) { + Widget _alertTile(BuildContext context, Alert alert, int index, {Animation? animation = null}) { final textTheme = Theme.of(context).textTheme; final alertColor = _alertColor(context, alert); Widget tile = InkWell( @@ -156,11 +156,11 @@ class __AlertsListState extends State<_AlertsList> { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16), - Text(_alertTitle(context, alert), style: textTheme.subtitle2.copyWith(color: alertColor)), + Text(_alertTitle(context, alert), style: textTheme.titleSmall?.copyWith(color: alertColor)), SizedBox(height: 4), - Text(alert.title, style: textTheme.subtitle1), + Text(alert.title, style: textTheme.titleMedium), SizedBox(height: 4), - Text(_formatDate(context, alert.actionDate), style: textTheme.subtitle2), + Text(_formatDate(context, alert.actionDate!) ?? '', style: textTheme.titleSmall), SizedBox(height: 12), ], ), @@ -183,7 +183,7 @@ class __AlertsListState extends State<_AlertsList> { if (animation != null) { tile = SizeTransition( - sizeFactor: animation, + sizeFactor: animation as Animation, axis: Axis.vertical, child: tile, ); @@ -212,7 +212,7 @@ class __AlertsListState extends State<_AlertsList> { Color _alertColor(BuildContext context, Alert alert) { if (alert.isAlertInfo()) return ParentColors.ash; - if (alert.isAlertPositive()) return ParentTheme.of(context).defaultTheme.accentColor; + if (alert.isAlertPositive()) return ParentTheme.of(context)!.defaultTheme.colorScheme.secondary; if (alert.isAlertNegative()) return ParentColors.failure; return ParentColors.failure; @@ -220,8 +220,8 @@ class __AlertsListState extends State<_AlertsList> { String _alertTitle(BuildContext context, Alert alert) { final l10n = L10n(context); - final threshold = _data.thresholds?.getThreshold(alert.alertType)?.threshold; - String title; + final threshold = _data?.thresholds?.getThreshold(alert.alertType)?.threshold ?? ''; + String title = ''; switch (alert.alertType) { case AlertType.institutionAnnouncement: title = l10n.institutionAnnouncement; @@ -252,7 +252,7 @@ class __AlertsListState extends State<_AlertsList> { return title; } - String _formatDate(BuildContext context, DateTime date) { + String? _formatDate(BuildContext context, DateTime date) { return date.l10nFormat(L10n(context).dateAtTime); } @@ -273,7 +273,7 @@ class __AlertsListState extends State<_AlertsList> { final readAlert = await widget._interactor.markAlertRead( widget._student.id, alert.id); - setState(() => _data.alerts.setRange(index, index + 1, [readAlert])); + setState(() => _data!.alerts!.setRange(index, index + 1, [readAlert!])); locator().update(widget._student.id); } } @@ -281,13 +281,13 @@ class __AlertsListState extends State<_AlertsList> { void _dismissAlert(Alert alert) async { _markAlertDismissed(alert); - int itemIndex = _data.alerts.indexOf(alert); + int itemIndex = _data!.alerts!.indexOf(alert); - _listKey.currentState.removeItem( + _listKey.currentState?.removeItem( itemIndex, (context, animation) => _alertTile(context, alert, itemIndex, animation: animation), duration: const Duration(milliseconds: 200)); - setState(() => _data.alerts.remove(alert)); + setState(() => _data!.alerts!.remove(alert)); } void _markAlertDismissed(Alert alert) async { diff --git a/apps/flutter_parent/lib/screens/announcements/announcement_details_interactor.dart b/apps/flutter_parent/lib/screens/announcements/announcement_details_interactor.dart index 6a1b26783e..191e1645db 100644 --- a/apps/flutter_parent/lib/screens/announcements/announcement_details_interactor.dart +++ b/apps/flutter_parent/lib/screens/announcements/announcement_details_interactor.dart @@ -30,7 +30,7 @@ class AnnouncementDetailsInteractor { CourseApi _courseApi() => locator(); - Future getAnnouncement( + Future getAnnouncement( String announcementId, AnnouncementType type, String courseId, @@ -38,10 +38,13 @@ class AnnouncementDetailsInteractor { bool forceRefresh, ) async { if (type == AnnouncementType.COURSE) { - Announcement announcement = + Announcement? announcement = await _announcementApi().getCourseAnnouncement(courseId, announcementId, forceRefresh); - Course course = await _courseApi().getCourse(courseId); + Course? course = await _courseApi().getCourse(courseId); + if (announcement == null || course == null) { + return null; + } return AnnouncementViewState( course.name, @@ -51,9 +54,12 @@ class AnnouncementDetailsInteractor { announcement.attachments.isNotEmpty ? announcement.attachments.first.toAttachment() : null, ); } else { - AccountNotification accountNotification = + AccountNotification? accountNotification = await _announcementApi().getAccountNotification(announcementId, forceRefresh); + if (accountNotification == null) + return null; + return AnnouncementViewState( institutionToolbarTitle, accountNotification.subject, diff --git a/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart b/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart index 3a2e1e3639..71a9c73bea 100644 --- a/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart +++ b/apps/flutter_parent/lib/screens/announcements/announcement_details_screen.dart @@ -32,17 +32,16 @@ class AnnouncementDetailScreen extends StatefulWidget { final String courseId; final AnnouncementType announcementType; - AnnouncementDetailScreen(this.announcementId, this.announcementType, this.courseId, BuildContext context, {Key key}) - : super(key: key); + AnnouncementDetailScreen(this.announcementId, this.announcementType, this.courseId, BuildContext? context, {super.key}); @override State createState() => _AnnouncementDetailScreenState(); } class _AnnouncementDetailScreenState extends State { - Future _announcementFuture; + Future? _announcementFuture; - Future _loadAnnouncement(BuildContext context, {bool forceRefresh = false}) => + Future _loadAnnouncement(BuildContext context, {bool forceRefresh = false}) => _interactor.getAnnouncement( widget.announcementId, widget.announcementType, @@ -60,7 +59,7 @@ class _AnnouncementDetailScreenState extends State { } return FutureBuilder( future: _announcementFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container(color: Theme.of(context).scaffoldBackgroundColor, child: LoadingIndicator()); } @@ -68,7 +67,7 @@ class _AnnouncementDetailScreenState extends State { if (snapshot.hasError || snapshot.data == null) { return _error(); } else { - return _announcementScaffold(snapshot.data); + return _announcementScaffold(snapshot.data!); } }, ); @@ -80,11 +79,11 @@ class _AnnouncementDetailScreenState extends State { title: Text(announcementViewState.toolbarTitle), ), body: RefreshIndicator( - onRefresh: () { + onRefresh: () async { setState(() { _announcementFuture = _loadAnnouncement(context, forceRefresh: true); }); - return _announcementFuture.catchError((_) {}); + await _announcementFuture?.catchError((_) { return Future.value(null); }); }, child: _announcementBody(announcementViewState)), ); @@ -96,12 +95,12 @@ class _AnnouncementDetailScreenState extends State { children: [ Text( announcementViewState.announcementTitle, - style: Theme.of(context).textTheme.headline4, + style: Theme.of(context).textTheme.headlineMedium, ), SizedBox(height: 4), Text( - announcementViewState.postedAt.l10nFormat(L10n(context).dateAtTime), - style: Theme.of(context).textTheme.caption, + announcementViewState.postedAt.l10nFormat(L10n(context).dateAtTime) ?? '', + style: Theme.of(context).textTheme.bodySmall, ), SizedBox(height: 16), Divider(), @@ -123,7 +122,7 @@ class _AnnouncementDetailScreenState extends State { })); } - Widget _attachmentsWidget(BuildContext context, Attachment attachment) { + Widget _attachmentsWidget(BuildContext context, Attachment? attachment) { if (attachment == null) return Container(); return Container( height: 108, diff --git a/apps/flutter_parent/lib/screens/announcements/announcement_view_state.dart b/apps/flutter_parent/lib/screens/announcements/announcement_view_state.dart index 102a9fb8f4..4e9d320725 100644 --- a/apps/flutter_parent/lib/screens/announcements/announcement_view_state.dart +++ b/apps/flutter_parent/lib/screens/announcements/announcement_view_state.dart @@ -20,8 +20,8 @@ class AnnouncementViewState { final String _toolbarTitle; final String _announcementTitle; final String _announcementMessage; - final DateTime _postedAt; - final Attachment _attachment; + final DateTime? _postedAt; + final Attachment? _attachment; AnnouncementViewState( this._toolbarTitle, @@ -37,7 +37,7 @@ class AnnouncementViewState { String get announcementMessage => _announcementMessage; - DateTime get postedAt => _postedAt; + DateTime? get postedAt => _postedAt; - Attachment get attachment => _attachment; + Attachment? get attachment => _attachment; } diff --git a/apps/flutter_parent/lib/screens/assignments/assignment_details_interactor.dart b/apps/flutter_parent/lib/screens/assignments/assignment_details_interactor.dart index 0d81ef7334..ac21967737 100644 --- a/apps/flutter_parent/lib/screens/assignments/assignment_details_interactor.dart +++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_interactor.dart @@ -23,11 +23,11 @@ import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class AssignmentDetailsInteractor { - Future loadAssignmentDetails( + Future loadAssignmentDetails( bool forceRefresh, String courseId, String assignmentId, - String studentId, + String? studentId, ) async { final course = locator().getCourse(courseId, forceRefresh: forceRefresh); final assignment = locator().getAssignment(courseId, assignmentId, forceRefresh: forceRefresh); @@ -53,19 +53,24 @@ class AssignmentDetailsInteractor { ); } - Future loadReminder(String assignmentId) async { - final reminder = await locator().getByItem( - ApiPrefs.getDomain(), - ApiPrefs.getUser().id, - Reminder.TYPE_ASSIGNMENT, - assignmentId, - ); + Future loadReminder(String assignmentId) async { + Reminder? reminder = null; + String? domain = ApiPrefs.getDomain(); + String? id = ApiPrefs.getUser()?.id; + if (domain != null && id != null) { + reminder = await locator().getByItem( + domain, + id, + Reminder.TYPE_ASSIGNMENT, + assignmentId, + ); + } /* If the user dismisses a reminder notification without tapping it, then NotificationUtil won't have a chance to remove it from the database. Given that we cannot time travel (yet), if the reminder we just retrieved has a date set in the past then we will opt to delete it here. */ if (reminder?.date?.isBefore(DateTime.now()) == true) { - await deleteReminder(reminder); + await deleteReminder(reminder!); return null; } @@ -77,12 +82,12 @@ class AssignmentDetailsInteractor { DateTime date, String assignmentId, String courseId, - String title, + String? title, String body, ) async { var reminder = Reminder((b) => b ..userDomain = ApiPrefs.getDomain() - ..userId = ApiPrefs.getUser().id + ..userId = ApiPrefs.getUser()?.id ..type = Reminder.TYPE_ASSIGNMENT ..itemId = assignmentId ..courseId = courseId @@ -90,21 +95,24 @@ class AssignmentDetailsInteractor { ); // Saving to the database will generate an ID for this reminder - reminder = await locator().insert(reminder); + var insertedReminder = await locator().insert(reminder); + if (insertedReminder != null) { + reminder = insertedReminder; + await locator().scheduleReminder(l10n, title, body, reminder); + } - await locator().scheduleReminder(l10n, title, body, reminder); } - Future deleteReminder(Reminder reminder) async { + Future deleteReminder(Reminder? reminder) async { if (reminder == null) return; - await locator().deleteNotification(reminder.id); - await locator().deleteById(reminder.id); + await locator().deleteNotification(reminder.id!); + await locator().deleteById(reminder.id!); } } class AssignmentDetails { - final Course course; - final Assignment assignment; + final Course? course; + final Assignment? assignment; AssignmentDetails({this.course, this.assignment}); } diff --git a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart index 72d0fa5a88..1a3ed256ed 100644 --- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart +++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart @@ -35,12 +35,10 @@ class AssignmentDetailsScreen extends StatefulWidget { final String assignmentId; const AssignmentDetailsScreen({ - Key key, - @required this.courseId, - @required this.assignmentId, - }) : assert(courseId != null), - assert(assignmentId != null), - super(key: key); + required this.courseId, + required this.assignmentId, + super.key + }); @override _AssignmentDetailsScreenState createState() => _AssignmentDetailsScreenState(); @@ -50,10 +48,10 @@ class _AssignmentDetailsScreenState extends State { GlobalKey _refreshKey = GlobalKey(); // State variables - Future _assignmentFuture; - Future _reminderFuture; - Future _animationFuture; - User _currentStudent; + late Future _assignmentFuture; + late Future _reminderFuture; + Future? _animationFuture; + User? _currentStudent; @override void initState() { @@ -65,22 +63,22 @@ class _AssignmentDetailsScreenState extends State { AssignmentDetailsInteractor get _interactor => locator(); - Future _loadAssignment({bool forceRefresh = false}) => _interactor.loadAssignmentDetails( + Future _loadAssignment({bool forceRefresh = false}) => _interactor.loadAssignmentDetails( forceRefresh, widget.courseId, widget.assignmentId, - _currentStudent.id, + _currentStudent?.id, ); - Future _loadReminder() => _interactor.loadReminder(widget.assignmentId); + Future _loadReminder() => _interactor.loadReminder(widget.assignmentId); @override Widget build(BuildContext context) { return FutureBuilder( future: _assignmentFuture, - builder: (context, AsyncSnapshot snapshot) => Scaffold( + builder: (context, AsyncSnapshot snapshot) => Scaffold( appBar: _appBar(snapshot), - floatingActionButton: snapshot.hasData && snapshot.data.assignment != null ? _fab(snapshot) : null, + floatingActionButton: snapshot.hasData && snapshot.data?.assignment != null ? _fab(snapshot) : null, body: RefreshIndicator( key: _refreshKey, child: _body(snapshot), @@ -96,32 +94,32 @@ class _AssignmentDetailsScreenState extends State { ); } - Widget _appBar(AsyncSnapshot snapshot) => AppBar( - bottom: ParentTheme.of(context).appBarDivider(), + AppBar _appBar(AsyncSnapshot snapshot) => AppBar( + bottom: ParentTheme.of(context)?.appBarDivider(), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(L10n(context).assignmentDetailsTitle), if (snapshot.hasData) - Text(snapshot.data.course?.name ?? '', - style: Theme.of(context).primaryTextTheme.caption, key: Key("assignment_details_coursename")), + Text(snapshot.data?.course?.name ?? '', + style: Theme.of(context).primaryTextTheme.bodySmall?.copyWith(color: Colors.white), key: Key("assignment_details_coursename")), ], ), ); - Widget _fab(AsyncSnapshot snapshot) { + Widget _fab(AsyncSnapshot snapshot) { return FloatingActionButton( - onPressed: () => _sendMessage(snapshot.data), + onPressed: () => _sendMessage(snapshot.data!), tooltip: L10n(context).assignmentMessageHint, child: Padding(padding: const EdgeInsets.only(left: 4, top: 4), child: Icon(CanvasIconsSolid.comment)), ); } - Widget _body(AsyncSnapshot snapshot) { + Widget _body(AsyncSnapshot snapshot) { if (snapshot.hasError) { return ErrorPandaWidget( L10n(context).unexpectedError, - () => _refreshKey.currentState.show(), + () => _refreshKey.currentState?.show(), ); } @@ -132,14 +130,14 @@ class _AssignmentDetailsScreenState extends State { final textTheme = Theme.of(context).textTheme; final l10n = L10n(context); - final course = snapshot.data.course; + final course = snapshot.data?.course; final restrictQuantitativeData = course?.settings?.restrictQuantitativeData ?? false; - final assignment = snapshot.data.assignment; - final submission = assignment.submission(_currentStudent.id); + final assignment = snapshot.data!.assignment!; + final submission = assignment.submission(_currentStudent?.id); final fullyLocked = assignment.isFullyLocked; final showStatus = assignment.isSubmittable() || submission?.isGraded() == true; final submitted = submission?.submittedAt != null; - final submittedColor = submitted ? ParentTheme.of(context).successColor : textTheme.caption.color; + final submittedColor = submitted ? ParentTheme.of(context)?.successColor : textTheme.bodySmall?.color; final points = (assignment.pointsPossible.toInt() == assignment.pointsPossible) ? assignment.pointsPossible.toInt().toString() @@ -153,24 +151,24 @@ class _AssignmentDetailsScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ ..._rowTile( - title: assignment.name, - titleStyle: textTheme.headline4, + title: assignment.name ?? '', + titleStyle: textTheme.headlineMedium!, child: Row( children: [ if (!restrictQuantitativeData) Text(l10n.assignmentTotalPoints(points), - style: textTheme.caption, + style: textTheme.bodySmall, semanticsLabel: l10n.assignmentTotalPointsAccessible(points), key: Key("assignment_details_total_points")), if (showStatus && !restrictQuantitativeData) SizedBox(width: 16), - if (showStatus) _statusIcon(submitted, submittedColor), + if (showStatus) _statusIcon(submitted, submittedColor!), if (showStatus) SizedBox(width: 8), if (showStatus) Text( !submitted ? l10n.assignmentNotSubmittedLabel : submission?.isGraded() == true ? l10n.assignmentGradedLabel : l10n.assignmentSubmittedLabel, - style: textTheme.caption.copyWith( + style: textTheme.bodySmall?.copyWith( color: submittedColor, ), key: Key("assignment_details_status")), @@ -181,8 +179,8 @@ class _AssignmentDetailsScreenState extends State { Divider(), ..._rowTile( title: l10n.assignmentDueLabel, - child: Text(_dateFormat(assignment?.dueAt?.toLocal()) ?? l10n.noDueDate, - style: textTheme.subtitle1, key: Key("assignment_details_due_date")), + child: Text(_dateFormat(assignment.dueAt?.toLocal()) ?? l10n.noDueDate, + style: textTheme.titleMedium, key: Key("assignment_details_due_date")), ), ], GradeCell.forSubmission(context, course, assignment, submission), @@ -192,8 +190,8 @@ class _AssignmentDetailsScreenState extends State { title: l10n.assignmentRemindMeLabel, child: FutureBuilder( future: _reminderFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - Reminder reminder = snapshot.data; + builder: (BuildContext context, AsyncSnapshot snapshot) { + Reminder? reminder = snapshot.data; return SwitchListTile( contentPadding: EdgeInsets.zero, value: reminder != null, @@ -201,15 +199,15 @@ class _AssignmentDetailsScreenState extends State { reminder?.date == null ? L10n(context).assignmentRemindMeDescription : L10n(context).assignmentRemindMeSet, - style: textTheme.subtitle1, + style: textTheme.titleMedium, ), subtitle: reminder == null ? null : Padding( padding: const EdgeInsets.only(top: 8), child: Text( - _dateFormat(reminder?.date?.toLocal()), - style: textTheme.subtitle1.copyWith(color: ParentTheme.of(context).studentColor), + _dateFormat(reminder.date?.toLocal()) ?? '', + style: textTheme.titleMedium?.copyWith(color: ParentTheme.of(context)?.studentColor), ), ), onChanged: (checked) => _handleAlarmSwitch(context, assignment, checked, reminder), @@ -270,24 +268,24 @@ class _AssignmentDetailsScreenState extends State { width: 18, height: 18, decoration: BoxDecoration(shape: BoxShape.circle, color: submittedColor), - child: Icon(CanvasIconsSolid.check, color: Theme.of(context).accentIconTheme.color, size: 8), + child: Icon(CanvasIconsSolid.check, color: Theme.of(context).colorScheme.onPrimary, size: 8), ); } - List _rowTile({String title, TextStyle titleStyle, Widget child}) { + List _rowTile({String? title, TextStyle? titleStyle, Widget? child}) { return [ SizedBox(height: 16), - Text(title ?? '', style: titleStyle ?? Theme.of(context).textTheme.overline), + Text(title ?? '', style: titleStyle ?? Theme.of(context).textTheme.labelSmall), SizedBox(height: 8), - child, + if (child != null) child, SizedBox(height: 16), ]; } List _lockedRow(Assignment assignment) { - String message = null; - if (assignment.lockInfo.hasModuleName) { - message = L10n(context).assignmentLockedModule(assignment.lockInfo.contextModule.name); + String? message = null; + if (assignment.lockInfo?.hasModuleName == true) { + message = L10n(context).assignmentLockedModule(assignment.lockInfo!.contextModule!.name!); } else if (assignment.isFullyLocked || (assignment.lockExplanation?.isNotEmpty == true && assignment.lockAt?.isBefore(DateTime.now()) == true)) { message = assignment.lockExplanation; @@ -299,21 +297,21 @@ class _AssignmentDetailsScreenState extends State { Divider(), ..._rowTile( title: L10n(context).assignmentLockLabel, - child: Text(message, style: Theme.of(context).textTheme.subtitle1), + child: Text(message!, style: Theme.of(context).textTheme.titleMedium), ), ]; } - String _dateFormat(DateTime time) => time.l10nFormat(L10n(context).dateAtTime); + String? _dateFormat(DateTime? time) => time?.l10nFormat(L10n(context).dateAtTime); - _handleAlarmSwitch(BuildContext context, Assignment assignment, bool checked, Reminder reminder) async { + _handleAlarmSwitch(BuildContext context, Assignment assignment, bool checked, Reminder? reminder) async { if (reminder != null) await _interactor.deleteReminder(reminder); if (checked) { var now = DateTime.now(); - var initialDate = assignment.dueAt?.isAfter(now) == true ? assignment.dueAt.toLocal() : now; + var initialDate = assignment.dueAt?.isAfter(now) == true ? assignment.dueAt!.toLocal() : now; - DateTime date; - TimeOfDay time; + DateTime? date; + TimeOfDay? time; date = await showDatePicker( context: context, @@ -328,13 +326,13 @@ class _AssignmentDetailsScreenState extends State { if (date != null && time != null) { DateTime reminderDate = DateTime(date.year, date.month, date.day, time.hour, time.minute); - var body = assignment.dueAt.l10nFormat(L10n(context).dueDateAtTime) ?? L10n(context).noDueDate; + var body = assignment.dueAt?.l10nFormat(L10n(context).dueDateAtTime) ?? L10n(context).noDueDate; await _interactor.createReminder( L10n(context), reminderDate, assignment.id, assignment.courseId, - assignment.name, + assignment.name ?? '', body, ); } @@ -347,9 +345,14 @@ class _AssignmentDetailsScreenState extends State { } _sendMessage(AssignmentDetails details) { - String subject = L10n(context).assignmentSubjectMessage(_currentStudent.name, details.assignment.name); - String postscript = L10n(context).messageLinkPostscript(_currentStudent.name, details.assignment.htmlUrl); - Widget screen = CreateConversationScreen(widget.courseId, _currentStudent.id, subject, postscript); - locator.get().push(context, screen); + if (_currentStudent != null) { + String subject = L10n(context).assignmentSubjectMessage( + _currentStudent?.name ?? '', details.assignment?.name ?? ''); + String postscript = L10n(context).messageLinkPostscript( + _currentStudent?.name ?? '', details.assignment?.htmlUrl ?? ''); + Widget screen = CreateConversationScreen( + widget.courseId, _currentStudent!.id, subject, postscript); + locator.get().push(context, screen); + } } } diff --git a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart index 1f74cdfb8b..9d8bb559bf 100644 --- a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart +++ b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart @@ -24,22 +24,21 @@ import 'package:percent_indicator/circular_percent_indicator.dart'; class GradeCell extends StatelessWidget { final GradeCellData data; - const GradeCell(this.data, {Key key}) : super(key: key); + const GradeCell(this.data, {super.key}); GradeCell.forSubmission( BuildContext context, - Course course, - Assignment assignment, - Submission submission, { - Key key, + Course? course, + Assignment? assignment, + Submission? submission, { + super.key, }) : data = GradeCellData.forSubmission( course, assignment, submission, Theme.of(context), L10n(context), - ), - super(key: key); + ); @override Widget build(BuildContext context) { @@ -53,7 +52,7 @@ class GradeCell extends StatelessWidget { children: [ Divider(), SizedBox(height: 8), - Text(L10n(context).assignmentGradeLabel, style: Theme.of(context).textTheme.overline), + Text(L10n(context).assignmentGradeLabel, style: Theme.of(context).textTheme.labelSmall), SizedBox(height: 8), data.state == GradeCellState.submitted ? _submitted(context, data) : _graded(context, data), SizedBox(height: 8), @@ -68,7 +67,7 @@ class GradeCell extends StatelessWidget { child: Column( children: [ Text(L10n(context).submissionStatusSuccessTitle, - style: Theme.of(context).textTheme.headline5.copyWith(color: ParentTheme.of(context).successColor), + style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: ParentTheme.of(context)?.successColor), key: Key("grade-cell-submit-status")), SizedBox(height: 6), Text(data.submissionText, textAlign: TextAlign.center), @@ -87,9 +86,9 @@ class GradeCell extends StatelessWidget { alignment: Alignment.center, children: [ CircularPercentIndicator( - radius: 128, + radius: 64, progressColor: data.accentColor, - backgroundColor: ParentTheme.of(context).nearSurfaceColor, + backgroundColor: ParentTheme.of(context)!.nearSurfaceColor, percent: data.graphPercent, lineWidth: 3, animation: true, @@ -144,7 +143,7 @@ class GradeCell extends StatelessWidget { Text( data.grade, key: Key('grade-cell-grade'), - style: Theme.of(context).textTheme.headline4, + style: Theme.of(context).textTheme.headlineMedium, semanticsLabel: data.gradeContentDescription, ), if (data.outOf.isNotEmpty) Text(data.outOf, key: Key('grade-cell-out-of')), @@ -160,7 +159,7 @@ class GradeCell extends StatelessWidget { child: Text( data.finalGrade, key: Key('grade-cell-final-grade'), - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, ), ), ], diff --git a/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_interactor.dart b/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_interactor.dart index 185841afc5..7c9dc297ab 100644 --- a/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_interactor.dart +++ b/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_interactor.dart @@ -19,11 +19,11 @@ import 'package:flutter_parent/network/api/user_api.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class AcceptableUsePolicyInteractor { - Future getTermsOfService() { + Future getTermsOfService() { return locator().getTermsOfService(); } - Future acceptTermsOfUse() { + Future acceptTermsOfUse() { return locator().acceptUserTermsOfUse(); } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_screen.dart b/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_screen.dart index b212153883..6c4b2714fd 100644 --- a/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_screen.dart +++ b/apps/flutter_parent/lib/screens/aup/acceptable_use_policy_screen.dart @@ -12,7 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; @@ -22,7 +21,6 @@ import 'package:flutter_parent/screens/aup/acceptable_use_policy_interactor.dart import 'package:flutter_parent/utils/common_widgets/masquerade_ui.dart'; import 'package:flutter_parent/utils/common_widgets/web_view/html_description_screen.dart'; import 'package:flutter_parent/utils/design/canvas_icons.dart'; -import 'package:flutter_parent/utils/design/parent_colors.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; @@ -67,14 +65,13 @@ class _AcceptableUsePolicyState extends State { style: TextStyle(fontSize: 16), )), Divider(), - FlatButton( + TextButton( onPressed: _readPolicy, - padding: - EdgeInsets.symmetric(horizontal: 16, vertical: 12), + style: TextButton.styleFrom(padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12)), child: Row( children: [ Text(L10n(context).acceptableUsePolicyTitle, - style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16)), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontSize: 16)), Spacer(), Icon( CanvasIcons.arrow_open_right, @@ -108,12 +105,12 @@ class _AcceptableUsePolicyState extends State { _close() async { try { - await locator().logEvent(AnalyticsEventConstants.LOGOUT); - await ParentTheme.of(context).setSelectedStudent(null); + locator().logEvent(AnalyticsEventConstants.LOGOUT); + await ParentTheme.of(context)?.setSelectedStudent(null); await ApiPrefs.performLogout(app: ParentApp.of(context)); - MasqueradeUI.of(context).refresh(); - locator().pushRouteAndClearStack(context, PandaRouter.login()); + MasqueradeUI.of(context)?.refresh(); await FeaturesUtils.performLogout(); + locator().pushRouteAndClearStack(context, PandaRouter.login()); } catch (e) { // Just in case we experience any error we still need to go back to the login screen. locator().pushRouteAndClearStack(context, PandaRouter.login()); @@ -130,7 +127,7 @@ class _AcceptableUsePolicyState extends State { _interactor.getTermsOfService().then((termsOfService) => locator() .push( context, - HtmlDescriptionScreen(termsOfService.content, + HtmlDescriptionScreen(termsOfService?.content, L10n(context).acceptableUsePolicyTitle))); } diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_day_list_tile.dart b/apps/flutter_parent/lib/screens/calendar/calendar_day_list_tile.dart index 3df39bc929..cba1d87470 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_day_list_tile.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_day_list_tile.dart @@ -34,22 +34,27 @@ class CalendarDayListTile extends StatelessWidget { onTap: () { switch (_item.plannableType) { case 'assignment': - locator() - .pushRoute(context, PandaRouter.assignmentDetails(_item.courseId, _item.plannable.assignmentId)); + if (_item.courseId != null && _item.plannable.assignmentId != null) { + locator().pushRoute(context, PandaRouter.assignmentDetails(_item.courseId!, _item.plannable.assignmentId!)); + } break; case 'calendar_event': // Case where the observed user has a personal calendar event - locator().pushRoute(context, PandaRouter.eventDetails(_item.courseId, _item.plannable.id)); + if (_item.courseId != null) { + locator().pushRoute(context, PandaRouter.eventDetails(_item.courseId!, _item.plannable.id)); + } break; case 'quiz': // This is a quiz assignment, go to the assignment page - locator() - .pushRoute(context, PandaRouter.quizAssignmentDetails(_item.courseId, _item.plannable.assignmentId)); + if (_item.courseId != null && _item.plannable.assignmentId != null) { + locator().pushRoute(context, PandaRouter.quizAssignmentDetails(_item.courseId!, _item.plannable.assignmentId!)); + } break; case 'discussion_topic': // This is a discussion assignment, go to the assignment page - locator() - .pushRoute(context, PandaRouter.discussionDetails(_item.courseId, _item.plannable.assignmentId)); + if (_item.courseId != null && _item.plannable.assignmentId != null) { + locator().pushRoute(context, PandaRouter.discussionDetails(_item.courseId!, _item.plannable.assignmentId!)); + } break; // case 'quiz': TODO - keep in place for potentially moving back to planner api // if (_item.plannable.assignmentId != null) { @@ -89,9 +94,9 @@ class CalendarDayListTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16), - Text(_getContextName(context, _item), style: textTheme.caption), + Text(_getContextName(context, _item), style: textTheme.bodySmall), SizedBox(height: 2), - Text(_item.plannable.title, style: textTheme.subtitle1), + Text(_item.plannable.title, style: textTheme.titleMedium), ..._getDueDate(context, _item), ..._getPointsOrStatus(context, _item), SizedBox(height: 12), @@ -107,7 +112,7 @@ class CalendarDayListTile extends StatelessWidget { } String _getContextName(BuildContext context, PlannerItem item) { - if (item.contextName != null) return item.contextName; + if (item.contextName != null) return item.contextName!; // Planner notes don't have a context name so we'll use 'Planner Note' // TODO - Keep in place for potentially moving back to planner api @@ -117,7 +122,7 @@ class CalendarDayListTile extends StatelessWidget { } Widget _getIcon(BuildContext context, PlannerItem item) { - IconData icon; + IconData? icon = null; switch (item.plannableType) { case 'assignment': icon = CanvasIcons.assignment; @@ -139,15 +144,15 @@ class CalendarDayListTile extends StatelessWidget { // icon = CanvasIcons.note; // break; } - return Icon(icon, size: 20, semanticLabel: '', color: Theme.of(context).accentColor); + return Icon(icon, size: 20, semanticLabel: '', color: Theme.of(context).colorScheme.secondary); } List _getDueDate(BuildContext context, PlannerItem plannerItem) { if (plannerItem.plannable.dueAt != null) { return [ SizedBox(height: 4), - Text(plannerItem.plannable.dueAt.l10nFormat(L10n(context).dueDateAtTime), - style: Theme.of(context).textTheme.caption), + Text(plannerItem.plannable.dueAt!.l10nFormat(L10n(context).dueDateAtTime) ?? '', + style: Theme.of(context).textTheme.bodySmall), ]; } return []; @@ -155,8 +160,8 @@ class CalendarDayListTile extends StatelessWidget { List _getPointsOrStatus(BuildContext context, PlannerItem plannerItem) { var submissionStatus = plannerItem.submissionStatus; - String pointsOrStatus = null; - String semanticLabel = null; + String? pointsOrStatus = null; + String? semanticLabel = null; // Submission status can be null for non-assignment contexts like announcements if (submissionStatus != null) { if (submissionStatus.excused) { @@ -181,7 +186,7 @@ class CalendarDayListTile extends StatelessWidget { SizedBox(height: 4), Text( pointsOrStatus, - style: Theme.of(context).textTheme.caption.copyWith(color: Theme.of(context).accentColor), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.secondary), semanticsLabel: semanticLabel, ), ]; diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_day_planner.dart b/apps/flutter_parent/lib/screens/calendar/calendar_day_planner.dart index fbc3bdc5ef..8599c3ab50 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_day_planner.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_day_planner.dart @@ -44,7 +44,7 @@ class CalendarDayPlannerState extends State { } else if (!snapshot.hasData) { body = LoadingIndicator(); } else { - if (snapshot.data.isEmpty) { + if (snapshot.data!.isEmpty) { body = EmptyPandaWidget( svgPath: 'assets/svg/panda-no-events.svg', title: L10n(context).noEventsTitle, @@ -52,7 +52,7 @@ class CalendarDayPlannerState extends State { header: SizedBox(height: 32), ); } else { - body = CalendarDayList(snapshot.data); + body = CalendarDayList(snapshot.data!); } } diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_screen.dart b/apps/flutter_parent/lib/screens/calendar/calendar_screen.dart index c568895d28..278abb9b1b 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_screen.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_screen.dart @@ -27,22 +27,22 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:provider/provider.dart'; class CalendarScreen extends StatefulWidget { - final DateTime startDate; - final CalendarView startView; + final DateTime? startDate; + final CalendarView? startView; // Keys for the deep link parameter map passed in via DashboardScreen static final startDateKey = 'startDate'; static final startViewKey = 'startView'; - CalendarScreen({Key key, this.startDate, this.startView = CalendarView.Week}) : super(key: key); + CalendarScreen({this.startDate, this.startView = CalendarView.Week, super.key}); @override State createState() => CalendarScreenState(); } class CalendarScreenState extends State { - User _student; - PlannerFetcher _fetcher; + User? _student; + PlannerFetcher? _fetcher; @override void didChangeDependencies() { @@ -53,12 +53,12 @@ class CalendarScreenState extends State { _student = _selectedStudent; if (_fetcher == null) { _fetcher = PlannerFetcher( - userId: ApiPrefs.getUser().id, - userDomain: ApiPrefs.getDomain(), - observeeId: _student.id, + userId: ApiPrefs.getUser()!.id, + userDomain: ApiPrefs.getDomain()!, + observeeId: _student!.id, ); } else { - _fetcher.setObserveeId(_student.id); + _fetcher!.setObserveeId(_student!.id); } } } @@ -66,18 +66,18 @@ class CalendarScreenState extends State { @override Widget build(BuildContext context) { return CalendarWidget( - fetcher: _fetcher, + fetcher: _fetcher!, startingDate: widget.startDate, startingView: widget.startView, onFilterTap: () async { - Set currentContexts = await _fetcher.getContexts(); + Set currentContexts = await _fetcher!.getContexts(); Set updatedContexts = await locator.get().push( context, CalendarFilterListScreen(currentContexts), ); if (!SetEquality().equals(currentContexts, updatedContexts)) { // Sets are different - update - _fetcher.setContexts(updatedContexts); + _fetcher?.setContexts(updatedContexts); } }, dayBuilder: (BuildContext context, DateTime day) { diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day.dart index 30fa4aa6de..c890faf5df 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day.dart @@ -33,11 +33,11 @@ class CalendarDay extends StatelessWidget { final DaySelectedCallback onDaySelected; const CalendarDay({ - Key key, - @required this.date, - @required this.selectedDay, - @required this.onDaySelected, - }) : super(key: key); + required this.date, + required this.selectedDay, + required this.onDaySelected, + super.key + }); @override Widget build(BuildContext context) { @@ -46,19 +46,19 @@ class CalendarDay extends StatelessWidget { final isToday = date.isSameDayAs(DateTime.now()); final isSelected = date.isSameDayAs(selectedDay); - TextStyle textStyle = theme.textTheme.headline5; + TextStyle textStyle = theme.textTheme.headlineSmall!; if (date.isWeekend() || date.month != selectedDay.month) textStyle = textStyle.copyWith(color: ParentColors.ash); - BoxDecoration decoration = null; + BoxDecoration? decoration = null; if (isToday) { - textStyle = Theme.of(context).accentTextTheme.headline5; - decoration = BoxDecoration(color: theme.accentColor, shape: BoxShape.circle); + textStyle = Theme.of(context).textTheme.headlineSmall!.copyWith(color: theme.colorScheme.onPrimary); + decoration = BoxDecoration(color: theme.colorScheme.secondary, shape: BoxShape.circle); } else if (isSelected) { - textStyle = textStyle.copyWith(color: Theme.of(context).accentColor); + textStyle = textStyle.copyWith(color: Theme.of(context).colorScheme.secondary); decoration = BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all( - color: theme.accentColor, + color: theme.colorScheme.secondary, width: 2, ), ); @@ -67,11 +67,11 @@ class CalendarDay extends StatelessWidget { return Selector>>( selector: (_, fetcher) => fetcher.getSnapshotForDate(date), builder: (_, snapshot, __) { - int eventCount = snapshot.hasData ? snapshot.data.length : 0; + int eventCount = snapshot.hasData ? snapshot.data!.length : 0; return InkResponse( enableFeedback: true, - highlightColor: theme.accentColor.withOpacity(0.35), - splashColor: theme.accentColor.withOpacity(0.35), + highlightColor: theme.colorScheme.secondary.withOpacity(0.35), + splashColor: theme.colorScheme.secondary.withOpacity(0.35), onTap: () => onDaySelected(date), child: Container( height: dayHeight, @@ -110,7 +110,7 @@ class CalendarDay extends StatelessWidget { /// On success, show dots for activities if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { // Show at most three dots for events - int count = min(snapshot.data.length, 3); + int count = min(snapshot.data!.length, 3); int itemCount = count < 1 ? count : (count * 2) - 1; if (count == 0) return Container(); return Row( @@ -120,7 +120,7 @@ class CalendarDay extends StatelessWidget { return Container( width: 4, height: 4, - decoration: BoxDecoration(color: Theme.of(context).accentColor, shape: BoxShape.circle), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, shape: BoxShape.circle), ); }), ); @@ -132,10 +132,10 @@ class CalendarDay extends StatelessWidget { height: 4, child: _RepeatTween( duration: Duration(milliseconds: 350), - delay: Duration(milliseconds: 100 * (date.localDayOfWeek)), + delay: Duration(milliseconds: 100 * (date.localDayOfWeek!)), builder: (BuildContext context, Animation animation) { return ScaleTransition( - scale: animation, + scale: animation as Animation, child: Container( width: 4, height: 4, @@ -151,18 +151,18 @@ class CalendarDay extends StatelessWidget { class _RepeatTween extends StatefulWidget { final Duration duration; - final Duration delay; + final Duration? delay; final Widget Function(BuildContext context, Animation animation) builder; - const _RepeatTween({Key key, @required this.duration, this.delay = null, @required this.builder}) : super(key: key); + const _RepeatTween({required this.duration, this.delay = null, required this.builder, super.key}); @override __RepeatTweenState createState() => __RepeatTweenState(); } class __RepeatTweenState extends State<_RepeatTween> with SingleTickerProviderStateMixin { - AnimationController _controller; + late AnimationController _controller; Tween tween = Tween(begin: 0.0, end: 1.0); @override @@ -173,7 +173,7 @@ class __RepeatTweenState extends State<_RepeatTween> with SingleTickerProviderSt } _startWithDelay() async { - if (widget.delay != null) await Future.delayed(widget.delay); + if (widget.delay != null) await Future.delayed(widget.delay!); if (mounted) _controller.repeat(reverse: true); } diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day_of_week_headers.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day_of_week_headers.dart index 9efb9acd4a..d590941ef5 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day_of_week_headers.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_day_of_week_headers.dart @@ -18,12 +18,12 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:intl/intl.dart'; class DayOfWeekHeaders extends StatelessWidget { - static const double headerHeight = 14; + static const double headerHeight = 16; @override Widget build(BuildContext context) { - final weekendTheme = Theme.of(context).textTheme.subtitle2; - final weekdayTheme = weekendTheme.copyWith(color: ParentTheme.of(context).onSurfaceColor); + final weekendTheme = Theme.of(context).textTheme.titleSmall; + final weekdayTheme = weekendTheme?.copyWith(color: ParentTheme.of(context)?.onSurfaceColor); final symbols = DateFormat(null, supportedDateLocale).dateSymbols; final firstDayOfWeek = symbols.FIRSTDAYOFWEEK; diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_interactor.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_interactor.dart index f1e8bf5407..d98a11505b 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_interactor.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_interactor.dart @@ -18,8 +18,8 @@ import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class CalendarFilterListInteractor { - Future> getCoursesForSelectedStudent({bool isRefresh = false}) async { + Future?> getCoursesForSelectedStudent({bool isRefresh = false}) async { var courses = await locator().getObserveeCourses(forceRefresh: isRefresh); - return courses.where((course) => course.isValidForCurrentStudent(ApiPrefs.getCurrentStudent().id)).toList(); + return courses?.where((course) => course.isValidForCurrentStudent(ApiPrefs.getCurrentStudent()?.id)).toList(); } } diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_screen.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_screen.dart index 43d2d1b71e..ec5e4626b4 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_screen.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_screen.dart @@ -12,7 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/course.dart'; @@ -33,7 +32,7 @@ class CalendarFilterListScreen extends StatefulWidget { } class CalendarFilterListScreenState extends State { - Future> _coursesFuture; + late Future?> _coursesFuture; Set selectedContextIds = {}; // Public, to allow for testing final GlobalKey _refreshCoursesKey = new GlobalKey(); final GlobalKey _scaffoldKey = GlobalKey(); @@ -60,7 +59,7 @@ class CalendarFilterListScreenState extends State { key: _scaffoldKey, appBar: AppBar( title: Text(L10n(context).calendars), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -68,7 +67,7 @@ class CalendarFilterListScreenState extends State { SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text(L10n(context).calendarTapToFavoriteDesc, style: Theme.of(context).textTheme.bodyText2), + child: Text(L10n(context).calendarTapToFavoriteDesc, style: Theme.of(context).textTheme.bodyMedium), ), SizedBox(height: 24.0), Expanded(child: _body()) @@ -84,11 +83,11 @@ class CalendarFilterListScreenState extends State { future: _coursesFuture, builder: (context, snapshot) { Widget _body; - List _courses; + List?_courses; if (snapshot.hasError) { - _body = ErrorPandaWidget(L10n(context).errorLoadingCourses, () => _refreshCoursesKey.currentState.show()); + _body = ErrorPandaWidget(L10n(context).errorLoadingCourses, () => _refreshCoursesKey.currentState?.show()); } else if (snapshot.hasData) { - _courses = snapshot.data; + _courses = snapshot.data!; courseLength = _courses.length; if (selectedContextIds.isEmpty && selectAllIfEmpty) { // We only want to do this the first time we load, otherwise if the user ever deselects all the @@ -100,7 +99,7 @@ class CalendarFilterListScreenState extends State { selectedContextIds.addAll(tempList); selectAllIfEmpty = false; } - _body = (_courses == null || _courses.isEmpty) + _body = (_courses.isEmpty) ? EmptyPandaWidget( svgPath: 'assets/svg/panda-book.svg', title: L10n(context).noCoursesTitle, @@ -147,9 +146,8 @@ class CalendarFilterListScreenState extends State { } else { if (selectedContextIds.length == 1) { // The list cannot be empty, the calendar wouldn't do anything! - _scaffoldKey.currentState.removeCurrentSnackBar(); - _scaffoldKey.currentState - .showSnackBar(SnackBar(content: Text(L10n(context).minimumCalendarsError))); + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(L10n(context).minimumCalendarsError))); } else { selectedContextIds.remove(c.contextFilterId()); } @@ -171,7 +169,7 @@ class CalendarFilterListScreenState extends State { padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: Text( title, - style: Theme.of(context).textTheme.overline, + style: Theme.of(context).textTheme.bodyMedium, ), ); } @@ -179,10 +177,10 @@ class CalendarFilterListScreenState extends State { // Custom checkbox to better control the padding at the start class LabeledCheckbox extends StatelessWidget { const LabeledCheckbox({ - this.label, - this.padding, - this.value, - this.onChanged, + required this.label, + required this.padding, + required this.value, + required this.onChanged, }); final String label; @@ -202,12 +200,12 @@ class LabeledCheckbox extends StatelessWidget { children: [ Checkbox( value: value, - onChanged: (bool newValue) { + onChanged: (bool? newValue) { onChanged(newValue); }, ), SizedBox(width: 21.0), - Expanded(child: Text(label, style: Theme.of(context).textTheme.subtitle1)), + Expanded(child: Text(label, style: Theme.of(context).textTheme.titleMedium)), ], ), ), diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_month.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_month.dart index 2f1c6ec11a..e0cd2d5c75 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_month.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_month.dart @@ -28,20 +28,20 @@ class CalendarMonth extends StatefulWidget { final MonthExpansionNotifier monthExpansionListener; CalendarMonth({ - Key key, - @required this.year, - @required this.month, - @required this.selectedDay, - @required this.onDaySelected, - @required this.monthExpansionListener, - }) : super(key: key); + required this.year, + required this.month, + required this.selectedDay, + required this.onDaySelected, + required this.monthExpansionListener, + super.key, + }); /// The maximum possible height of this widget static double maxHeight = DayOfWeekHeaders.headerHeight + (6 * CalendarDay.dayHeight); static List generateWeekStarts(int year, int month) { DateTime firstDayOfMonth = DateTime(year, month); - DateTime firstDayOfWeek = firstDayOfMonth.withFirstDayOfWeek(); + DateTime firstDayOfWeek = firstDayOfMonth.withFirstDayOfWeek()!; List weekStarts = [firstDayOfWeek]; @@ -62,7 +62,7 @@ class CalendarMonth extends StatefulWidget { } class _CalendarMonthState extends State { - List weekStarts; + late List weekStarts; @override void initState() { @@ -74,7 +74,7 @@ class _CalendarMonthState extends State { Widget build(BuildContext context) { final weekWidgets = weekStarts.mapIndexed((index, weekStart) { final weekWidget = CalendarWeek( - firstDay: weekStart, + firstDay: weekStart!, selectedDay: widget.selectedDay, onDaySelected: widget.onDaySelected, displayDayOfWeekHeader: false, @@ -83,7 +83,7 @@ class _CalendarMonthState extends State { return ValueListenableBuilder( child: weekWidget, valueListenable: widget.monthExpansionListener, - builder: (BuildContext context, double value, Widget child) { + builder: (BuildContext context, double value, Widget? child) { final top = DayOfWeekHeaders.headerHeight + (value * index * CalendarDay.dayHeight); return Positioned( top: top, @@ -103,7 +103,7 @@ class _CalendarMonthState extends State { return Stack( children: [ DayOfWeekHeaders(), - ...weekWidgets, + ...weekWidgets!, ], ); } diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_week.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_week.dart index c9dbb61d4e..b2c3094a4b 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_week.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_week.dart @@ -27,13 +27,12 @@ class CalendarWeek extends StatelessWidget { final List days; CalendarWeek({ - Key key, - @required this.firstDay, - @required this.selectedDay, - @required this.onDaySelected, - @required this.displayDayOfWeekHeader, - }) : days = generateDays(firstDay), - super(key: key); + required this.firstDay, + required this.selectedDay, + required this.onDaySelected, + required this.displayDayOfWeekHeader, + super.key + }) : days = generateDays(firstDay); static List generateDays(DateTime firstDay) { return List.generate(7, (index) => DateTime(firstDay.year, firstDay.month, firstDay.day + index)); diff --git a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_widget.dart b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_widget.dart index 1448a0ba54..08e475fb87 100644 --- a/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_widget.dart +++ b/apps/flutter_parent/lib/screens/calendar/calendar_widget/calendar_widget.dart @@ -47,22 +47,22 @@ class CalendarWidget extends StatefulWidget { /// A [PlannerFetcher] that handles fetching planner events from the API final PlannerFetcher fetcher; - final VoidCallback onFilterTap; + final VoidCallback? onFilterTap; /// Starting DateTime, defaults to today's date - final DateTime startingDate; + final DateTime? startingDate; /// Starting view, either 'week' or 'calendar' - final CalendarView startingView; + final CalendarView? startingView; const CalendarWidget({ - Key key, - @required this.dayBuilder, - @required this.fetcher, + required this.dayBuilder, + required this.fetcher, this.onFilterTap, this.startingDate, this.startingView, - }) : super(key: key); + super.key + }); @override CalendarWidgetState createState() => CalendarWidgetState(); @@ -102,9 +102,9 @@ class CalendarWidgetState extends State with TickerProviderState Key _monthKey = GlobalKey(); // Page controllers - PageController _dayController; - PageController _weekController; - PageController _monthController; + late PageController _dayController; + late PageController _weekController; + late PageController _monthController; // Notifier that tracks the current month collapse/expand progress MonthExpansionNotifier _monthExpansionNotifier = MonthExpansionNotifier(0.0); @@ -125,13 +125,13 @@ class CalendarWidgetState extends State with TickerProviderState double get _monthExpansionHeight => _fullMonthHeight - CalendarWeek.weekHeight; // Controller for animating the month expand/collapse progress when the user presses the expand/collapse button - AnimationController _monthExpandAnimController; + late AnimationController _monthExpandAnimController; // Controller for animating the month expand/collapse progress when the user swipes vertically on the calendar - AnimationController _monthFlingAnimController; + late AnimationController? _monthFlingAnimController; // Controller for animating the full month height when switching between months that have a different number of weeks - AnimationController _monthHeightAdjustAnimController; + late AnimationController _monthHeightAdjustAnimController; // Returns the full month height for the month found at the specified month pager index static double _calculateFullMonthHeight(int monthIndex) { @@ -144,14 +144,14 @@ class CalendarWidgetState extends State with TickerProviderState static DateTime _dayForIndex(int index) { final today = DateTime.now(); final diff = index - _todayDayIndex; - return DateTime(today.year, today.month, today.day).add(Duration(days: diff)).roundToMidnight(); + return DateTime(today.year, today.month, today.day).add(Duration(days: diff)).roundToMidnight()!; } // Returns the DateTime that represents the first day of the week associated with the specified week pager index static DateTime _weekStartForIndex(int index) { final today = DateTime.now(); int weekOffset = index - _todayWeekIndex; - return DateTime(today.year, today.month, today.day + (weekOffset * 7)).withFirstDayOfWeek(); + return DateTime(today.year, today.month, today.day + (weekOffset * 7)).withFirstDayOfWeek()!; } // Returns the year and month associated with the specified month pager index @@ -179,7 +179,7 @@ class CalendarWidgetState extends State with TickerProviderState static int _weekIndexForDay(DateTime day) { final weekStart = day.withFirstDayOfWeek(); final thisWeekStart = DateTime.now().withFirstDayOfWeek(); - double weeksDiff = thisWeekStart.difference(weekStart).inDays / 7; + double weeksDiff = thisWeekStart!.difference(weekStart!).inDays / 7; return _todayWeekIndex - weeksDiff.round(); } @@ -220,8 +220,8 @@ class CalendarWidgetState extends State with TickerProviderState // Set up animation controller for expand/collapse fling animation _monthFlingAnimController = AnimationController(duration: CalendarWidget.animDuration, vsync: this); - _monthFlingAnimController.addListener(() { - _monthExpansionNotifier.value = _monthFlingAnimController.value; + _monthFlingAnimController?.addListener(() { + _monthExpansionNotifier.value = _monthFlingAnimController!.value; }); // Set up controllers for day, week, and month pagers @@ -232,7 +232,7 @@ class CalendarWidgetState extends State with TickerProviderState if (widget.startingDate != null) { WidgetsBinding.instance.addPostFrameCallback((_) { selectDay( - widget.startingDate, + widget.startingDate!, dayPagerBehavior: CalendarPageChangeBehavior.jump, weekPagerBehavior: CalendarPageChangeBehavior.jump, monthPagerBehavior: CalendarPageChangeBehavior.jump, @@ -298,21 +298,21 @@ class CalendarWidgetState extends State with TickerProviderState children: [ Text( DateFormat.y(supportedDateLocale).format(selectedDay), - style: Theme.of(context).textTheme.overline, + style: Theme.of(context).textTheme.labelSmall, ), Row( children: [ Text( DateFormat.MMMM(supportedDateLocale).format(selectedDay), - style: Theme.of(context).textTheme.headline4, + style: Theme.of(context).textTheme.headlineMedium, ), SizedBox(width: 10), Visibility( visible: _canExpandMonth, child: ValueListenableBuilder( - builder: (BuildContext context, value, Widget child) { + builder: (BuildContext context, value, Widget? child) { return DropdownArrow( - specificProgress: value, color: ParentTheme.of(context).onSurfaceColor); + specificProgress: value, color: ParentTheme.of(context)!.onSurfaceColor); }, valueListenable: _monthExpansionNotifier, ), @@ -334,7 +334,7 @@ class CalendarWidgetState extends State with TickerProviderState padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Text( L10n(context).calendars, - style: Theme.of(context).textTheme.caption.copyWith(color: Theme.of(context).accentColor), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.secondary), ), ), ), @@ -347,7 +347,7 @@ class CalendarWidgetState extends State with TickerProviderState return GestureDetector( onVerticalDragUpdate: _canExpandMonth ? (details) { - var expansionDiff = details.primaryDelta / _monthExpansionHeight; + var expansionDiff = details.primaryDelta! / _monthExpansionHeight; _monthExpansionNotifier.value = (_monthExpansionNotifier.value + expansionDiff).clamp(0.0, 1.0); } : null, @@ -355,8 +355,8 @@ class CalendarWidgetState extends State with TickerProviderState ? (details) { _monthFlingAnimController?.stop(); if (_isMonthExpanded) { - _monthFlingAnimController.value = _monthExpansionNotifier.value; - _monthFlingAnimController.fling(velocity: details.primaryVelocity / _monthExpansionHeight); + _monthFlingAnimController?.value = _monthExpansionNotifier.value; + _monthFlingAnimController?.fling(velocity: details.primaryVelocity! / _monthExpansionHeight); } } : null, @@ -368,7 +368,7 @@ class CalendarWidgetState extends State with TickerProviderState ], ), valueListenable: _monthExpansionNotifier, - builder: (BuildContext context, double value, Widget child) { + builder: (BuildContext context, double value, Widget? child) { return Container( height: CalendarWeek.weekHeight + (value * _monthExpansionHeight), child: child, @@ -392,18 +392,18 @@ class CalendarWidgetState extends State with TickerProviderState }, ); - GestureDragUpdateCallback updateCallback = null; - GestureDragEndCallback endCallback = null; + GestureDragUpdateCallback? updateCallback = null; + GestureDragEndCallback? endCallback = null; if (_isMonthExpanded) { updateCallback = (details) { - var expansionDiff = details.primaryDelta / _monthExpansionHeight; + var expansionDiff = details.primaryDelta! / _monthExpansionHeight; _monthExpansionNotifier.value = (_monthExpansionNotifier.value + expansionDiff).clamp(0.0, 1.0); }; endCallback = (details) { _monthFlingAnimController?.stop(); - _monthFlingAnimController.value = _monthExpansionNotifier.value; - _monthFlingAnimController.fling(velocity: details.primaryVelocity / _monthExpansionHeight); + _monthFlingAnimController?.value = _monthExpansionNotifier.value; + _monthFlingAnimController?.fling(velocity: details.primaryVelocity! / _monthExpansionHeight); }; } @@ -459,9 +459,9 @@ class CalendarWidgetState extends State with TickerProviderState Animation anim = tween.animate( CurvedAnimation(parent: _monthHeightAdjustAnimController, curve: CalendarWidget.animCurve), ); - VoidCallback listener; + VoidCallback? listener = null; listener = () { - if (anim.status == AnimationStatus.completed) anim.removeListener(listener); + if (anim.status == AnimationStatus.completed && listener != null) anim.removeListener(listener); _fullMonthHeight = anim.value * newHeight; _monthExpansionNotifier.notify(); }; @@ -474,9 +474,9 @@ class CalendarWidgetState extends State with TickerProviderState void selectDay( DateTime day, { - CalendarPageChangeBehavior dayPagerBehavior: CalendarPageChangeBehavior.jump, - CalendarPageChangeBehavior weekPagerBehavior: CalendarPageChangeBehavior.animate, - CalendarPageChangeBehavior monthPagerBehavior: CalendarPageChangeBehavior.animate, + CalendarPageChangeBehavior dayPagerBehavior = CalendarPageChangeBehavior.jump, + CalendarPageChangeBehavior weekPagerBehavior = CalendarPageChangeBehavior.animate, + CalendarPageChangeBehavior monthPagerBehavior = CalendarPageChangeBehavior.animate, }) { // Do nothing if the day is already selected if (selectedDay.isSameDayAs(day)) return; @@ -556,7 +556,7 @@ class CalendarWidgetState extends State with TickerProviderState List _a11yWeekButtons() { int index = _todayWeekIndex; if (_weekController.hasClients) { - index = _weekController.page.toInt(); + index = _weekController.page?.toInt() ?? 0; } final format = DateFormat.MMMMd(supportedDateLocale).add_y(); @@ -590,7 +590,7 @@ class CalendarWidgetState extends State with TickerProviderState List _a11yMonthButtons() { int index = _todayMonthIndex; if (_monthController.hasClients) { - index = _monthController.page.toInt(); + index = _monthController.page?.toInt() ?? 0; } final format = DateFormat.MMMM(supportedDateLocale).add_y(); @@ -649,10 +649,10 @@ class CalendarWidgetState extends State with TickerProviderState final Animation anim = tween.animate( CurvedAnimation(parent: _monthExpandAnimController, curve: CalendarWidget.animCurve), ); - VoidCallback listener; + VoidCallback? listener = null; listener = () { _monthExpansionNotifier.value = anim.value; - if (anim.status == AnimationStatus.completed) anim.removeListener(listener); + if (anim.status == AnimationStatus.completed && listener != null) anim.removeListener(listener); }; anim.addListener(listener); _monthExpandAnimController.forward(); diff --git a/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart b/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart index 6772d0fe0c..3092680b07 100644 --- a/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart +++ b/apps/flutter_parent/lib/screens/calendar/planner_fetcher.dart @@ -28,7 +28,7 @@ import 'package:flutter_parent/utils/db/calendar_filter_db.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class PlannerFetcher extends ChangeNotifier { - final Map>> daySnapshots = {}; + final Map>?> daySnapshots = {}; final Map failedMonths = {}; @@ -39,25 +39,24 @@ class PlannerFetcher extends ChangeNotifier { final String userId; - Future> courseListFuture; + Future?>? courseListFuture; bool firstFilterUpdateFlag = false; String _observeeId; String get observeeId => _observeeId; - PlannerFetcher({DateTime fetchFirst, @required String observeeId, @required this.userDomain, @required this.userId}) { - this._observeeId = observeeId; + PlannerFetcher({DateTime? fetchFirst, required String observeeId, required this.userDomain, required this.userId}): _observeeId = observeeId { if (fetchFirst != null) getSnapshotForDate(fetchFirst); } Future> getContexts() async { - CalendarFilter calendarFilter = await locator().getByObserveeId( + CalendarFilter? calendarFilter = await locator().getByObserveeId( userDomain, userId, _observeeId, ); - if (calendarFilter == null || (courseNameMap[_observeeId] == null || courseNameMap[_observeeId].isEmpty)) { + if (calendarFilter == null || (courseNameMap[_observeeId] == null || courseNameMap[_observeeId]!.isEmpty)) { // We need to fetch the courses for a couple of reasons: // First, scheduleItems don't have a context name. // Second, calendar is opposite planner, in that 0 contexts means no calendar items. In order to deal with that @@ -74,9 +73,9 @@ class PlannerFetcher extends ChangeNotifier { // Add the names to our map so we can fill those in later // We have to handle the case where their filter is from before the api migration, so we trim it down. - courses.forEach((course) => courseNameMap[_observeeId][course.id] = course.name); - var tempCourseList = courses.map((course) => course.contextFilterId()).toList(); - Set courseSet = Set.from(tempCourseList); + courses?.forEach((course) => courseNameMap[_observeeId]![course.id] = course.name); + var tempCourseList = courses?.map((course) => course.contextFilterId()).toList(); + Set courseSet = Set.from(tempCourseList ?? []); if (calendarFilter != null) { return calendarFilter.filters.toSet(); @@ -100,15 +99,15 @@ class PlannerFetcher extends ChangeNotifier { AsyncSnapshot> getSnapshotForDate(DateTime date) { final dayKey = dayKeyForDate(date); - AsyncSnapshot> daySnapshot = daySnapshots[dayKey]; + AsyncSnapshot>? daySnapshot = daySnapshots[dayKey]; if (daySnapshot != null) return daySnapshot; _beginMonthFetch(date); - return daySnapshots[dayKey]; + return daySnapshots[dayKey]!; } Future refreshItemsForDate(DateTime date) async { String dayKey = dayKeyForDate(date); - bool hasError = daySnapshots[dayKey].hasError; + bool hasError = daySnapshots[dayKey]?.hasError ?? true; bool monthFailed = failedMonths[monthKeyForYearMonth(date.year, date.month)] ?? false; if (hasError && monthFailed) { // Previous fetch failed, retry the whole month @@ -119,12 +118,12 @@ class PlannerFetcher extends ChangeNotifier { if (hasError) { daySnapshots[dayKey] = AsyncSnapshot>.nothing().inState(ConnectionState.waiting); } else { - daySnapshots[dayKey] = daySnapshots[dayKey].inState(ConnectionState.waiting); + daySnapshots[dayKey] = daySnapshots[dayKey]!.inState(ConnectionState.waiting); } notifyListeners(); try { final contexts = await getContexts(); - List items = await fetchPlannerItems(date.withStartOfDay(), date.withEndOfDay(), contexts, true); + List items = await fetchPlannerItems(date.withStartOfDay()!, date.withEndOfDay()!, contexts, true); daySnapshots[dayKey] = AsyncSnapshot>.withData(ConnectionState.done, items); } catch (e) { daySnapshots[dayKey] = AsyncSnapshot>.withError(ConnectionState.done, e); @@ -135,7 +134,7 @@ class PlannerFetcher extends ChangeNotifier { } _beginMonthFetch(DateTime date, {bool refresh = false}) { - final lastDayOfMonth = date.withEndOfMonth().day; + final lastDayOfMonth = date.withEndOfMonth()!.day; for (int i = 1; i <= lastDayOfMonth; i++) { var dayKey = dayKeyForYearMonthDay(date.year, date.month, i); daySnapshots[dayKey] = AsyncSnapshot>.nothing().inState(ConnectionState.waiting); @@ -147,7 +146,7 @@ class PlannerFetcher extends ChangeNotifier { try { final contexts = await getContexts(); List items = - await fetchPlannerItems(date.withStartOfMonth(), date.withEndOfMonth(), contexts, refresh); + await fetchPlannerItems(date.withStartOfMonth()!, date.withEndOfMonth()!, contexts, refresh); _completeMonth(items, date); } catch (e) { _failMonth(e, date); @@ -157,7 +156,7 @@ class PlannerFetcher extends ChangeNotifier { @visibleForTesting Future> fetchPlannerItems( DateTime startDate, DateTime endDate, Set contexts, bool refresh) async { - List> tempItems = await Future.wait([ + List?> tempItems = await Future.wait([ locator().getUserCalendarItems( _observeeId, startDate, @@ -175,23 +174,28 @@ class PlannerFetcher extends ChangeNotifier { forceRefresh: refresh, ), ]); - - List scheduleItems = tempItems[0] + tempItems[1]; + List? scheduleItems; + if (tempItems[0] == null || tempItems[1] == null) { + scheduleItems = []; + } + else { + scheduleItems = tempItems[0]! + tempItems[1]!; + } scheduleItems.retainWhere((it) => it.isHidden != true); // Exclude hidden items - return scheduleItems.map((item) => item.toPlannerItem(courseNameMap[_observeeId][item.getContextId()])).toList(); + return scheduleItems.map((item) => item.toPlannerItem(courseNameMap[_observeeId]![item.getContextId()])).toList(); } _completeMonth(List items, DateTime date) { failedMonths[monthKeyForDate(date)] = false; final Map> dayItems = {}; - final lastDayOfMonth = date.withEndOfMonth().day; + final lastDayOfMonth = date.withEndOfMonth()!.day; for (int i = 1; i <= lastDayOfMonth; i++) { dayItems[dayKeyForYearMonthDay(date.year, date.month, i)] = []; } items.forEach((item) { if (item.plannableDate != null) { - String dayKey = dayKeyForDate(item.plannableDate.toLocal()); - dayItems[dayKey].add(item); + String dayKey = dayKeyForDate(item.plannableDate!.toLocal()); + dayItems[dayKey]?.add(item); } }); @@ -203,7 +207,7 @@ class PlannerFetcher extends ChangeNotifier { _failMonth(Object error, DateTime date) { failedMonths[monthKeyForDate(date)] = true; - final lastDayOfMonth = date.withEndOfMonth().day; + final lastDayOfMonth = date.withEndOfMonth()!.day; for (int i = 1; i <= lastDayOfMonth; i++) { daySnapshots[dayKeyForYearMonthDay(date.year, date.month, i)] = AsyncSnapshot.withError(ConnectionState.done, error); diff --git a/apps/flutter_parent/lib/screens/courses/courses_interactor.dart b/apps/flutter_parent/lib/screens/courses/courses_interactor.dart index dd2b471f01..2580a177f5 100644 --- a/apps/flutter_parent/lib/screens/courses/courses_interactor.dart +++ b/apps/flutter_parent/lib/screens/courses/courses_interactor.dart @@ -18,10 +18,10 @@ import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class CoursesInteractor { - Future> getCourses({bool isRefresh = false, String studentId = null}) async { + Future?> getCourses({bool isRefresh = false, String? studentId = null}) async { var courses = await locator().getObserveeCourses(forceRefresh: isRefresh); var currentStudentId = studentId; - if (currentStudentId == null) currentStudentId = ApiPrefs.getCurrentStudent().id; - return courses.where((course) => course.isValidForCurrentStudent(currentStudentId)).toList(); + if (currentStudentId == null) currentStudentId = ApiPrefs.getCurrentStudent()?.id; + return courses?.where((course) => course.isValidForCurrentStudent(currentStudentId)).toList(); } } diff --git a/apps/flutter_parent/lib/screens/courses/courses_screen.dart b/apps/flutter_parent/lib/screens/courses/courses_screen.dart index 47e1c3401e..c3484287c7 100644 --- a/apps/flutter_parent/lib/screens/courses/courses_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/courses_screen.dart @@ -35,9 +35,9 @@ class CoursesScreen extends StatefulWidget { } class _CoursesScreenState extends State { - User _student; + User? _student; - Future> _coursesFuture; + Future?>? _coursesFuture; CoursesInteractor _interactor = locator(); @@ -55,8 +55,8 @@ class _CoursesScreenState extends State { } } - Future> _loadCourses({bool forceRefresh = false}) => - _interactor.getCourses(isRefresh: forceRefresh, studentId: _student?.id?.isEmpty == true ? null : _student.id); + Future?> _loadCourses({bool forceRefresh = false}) => + _interactor.getCourses(isRefresh: forceRefresh, studentId: _student?.id.isEmpty == true ? null : _student!.id); @override Widget build(BuildContext context) => _content(context); @@ -64,18 +64,18 @@ class _CoursesScreenState extends State { Widget _content(BuildContext context) { return FutureBuilder( future: _coursesFuture, - builder: (context, AsyncSnapshot> snapshot) { + builder: (context, AsyncSnapshot?> snapshot) { Widget _body; if (snapshot.hasError) { - _body = ErrorPandaWidget(L10n(context).errorLoadingCourses, () => _refreshKey.currentState.show()); + _body = ErrorPandaWidget(L10n(context).errorLoadingCourses, () => _refreshKey.currentState?.show()); } else if (snapshot.hasData) { - _body = (snapshot.data.isEmpty) + _body = (snapshot.data!.isEmpty) ? EmptyPandaWidget( svgPath: 'assets/svg/panda-book.svg', title: L10n(context).noCoursesTitle, subtitle: L10n(context).noCoursesMessage, ) - : _success(snapshot.data); + : _success(snapshot.data!); } else { return LoadingIndicator(); } @@ -104,11 +104,11 @@ class _CoursesScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 8), - Text(course.name ?? '', - style: Theme.of(context).textTheme.subtitle1, key: Key("${course.courseCode}_name")), + Text(course.name, + style: Theme.of(context).textTheme.titleMedium, key: Key("${course.courseCode}_name")), SizedBox(height: 2), Text(course.courseCode ?? '', - style: Theme.of(context).textTheme.caption, key: Key("${course.courseCode}_code")), + style: Theme.of(context).textTheme.bodySmall, key: Key("${course.courseCode}_code")), if (grade != null) SizedBox(height: 4), if (grade != null) grade, SizedBox(height: 8), @@ -118,13 +118,13 @@ class _CoursesScreenState extends State { }); } - Widget _courseGrade(context, Course course) { + Widget? _courseGrade(context, Course course) { CourseGrade grade = course.getCourseGrade(_student?.id); var format = NumberFormat.percentPattern(); format.maximumFractionDigits = 2; - if (grade.isCourseGradeLocked(forAllGradingPeriods: course?.enrollments?.any((enrollment) => enrollment.hasActiveGradingPeriod()) != true) || - (course?.settings?.restrictQuantitativeData == true && grade.currentGrade() == null)) { + if (grade.isCourseGradeLocked(forAllGradingPeriods: course.enrollments?.any((enrollment) => enrollment.hasActiveGradingPeriod()) != true) || + (course.settings?.restrictQuantitativeData == true && grade.currentGrade() == null)) { return null; } // If there is no current grade, return 'No grade' @@ -133,14 +133,14 @@ class _CoursesScreenState extends State { var text = grade.noCurrentGrade() ? L10n(context).noGrade : grade.currentGrade()?.isNotEmpty == true - ? grade.currentGrade() - : format.format(grade.currentScore() / 100); + ? grade.currentGrade()! + : format.format(grade.currentScore()! / 100); return Text( text, key: Key("${course.courseCode}_grade"), style: TextStyle( - color: Theme.of(context).accentColor, + color: Theme.of(context).colorScheme.secondary, fontSize: 16, fontWeight: FontWeight.w500, ), @@ -151,10 +151,10 @@ class _CoursesScreenState extends State { locator().pushRoute(context, PandaRouter.courseDetails(course.id)); } - Future _refresh() { + Future _refresh() async { setState(() { _coursesFuture = _loadCourses(forceRefresh: true); }); - return _coursesFuture; + await _coursesFuture; } } diff --git a/apps/flutter_parent/lib/screens/courses/details/course_details_interactor.dart b/apps/flutter_parent/lib/screens/courses/details/course_details_interactor.dart index 5e8402e61a..2d69f3d7da 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_details_interactor.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_details_interactor.dart @@ -28,33 +28,33 @@ import 'package:flutter_parent/network/api/page_api.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class CourseDetailsInteractor { - Future loadCourse(String courseId, {bool forceRefresh = false}) { + Future loadCourse(String courseId, {bool forceRefresh = false}) { return locator().getCourse(courseId, forceRefresh: forceRefresh); } - Future> loadCourseTabs(String courseId, {bool forceRefresh = false}) => + Future?> loadCourseTabs(String courseId, {bool forceRefresh = false}) => locator().getCourseTabs(courseId, forceRefresh: forceRefresh); - Future loadCourseSettings(String courseId, {bool forceRefresh = false}) => + Future loadCourseSettings(String courseId, {bool forceRefresh = false}) => locator().getCourseSettings(courseId, forceRefresh: forceRefresh); - Future> loadAssignmentGroups(String courseId, String studentId, String gradingPeriodId, + Future?> loadAssignmentGroups(String courseId, String? studentId, String? gradingPeriodId, {bool forceRefresh = false}) { return locator().getAssignmentGroupsWithSubmissionsDepaginated(courseId, studentId, gradingPeriodId, forceRefresh: forceRefresh); } - Future loadGradingPeriods(String courseId, {bool forceRefresh = false}) { + Future loadGradingPeriods(String courseId, {bool forceRefresh = false}) { return locator().getGradingPeriods(courseId, forceRefresh: forceRefresh); } - Future> loadEnrollmentsForGradingPeriod(String courseId, String studentId, String gradingPeriodId, + Future?> loadEnrollmentsForGradingPeriod(String courseId, String? studentId, String? gradingPeriodId, {bool forceRefresh = false}) { return locator() .getEnrollmentsByGradingPeriod(courseId, studentId, gradingPeriodId, forceRefresh: forceRefresh); } - Future> loadScheduleItems(String courseId, String type, bool refresh) { + Future?> loadScheduleItems(String courseId, String type, bool refresh) { return locator().getAllCalendarEvents( allEvents: true, type: type, @@ -63,6 +63,6 @@ class CourseDetailsInteractor { ); } - Future loadFrontPage(String courseId, {bool forceRefresh = false}) => + Future loadFrontPage(String courseId, {bool forceRefresh = false}) => locator().getCourseFrontPage(courseId, forceRefresh: forceRefresh); } diff --git a/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart b/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart index b14a450c75..d51733c091 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_details_model.dart @@ -28,21 +28,22 @@ import 'package:flutter_parent/utils/base_model.dart'; import 'package:flutter_parent/utils/core_extensions/list_extensions.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:tuple/tuple.dart'; +import 'package:collection/collection.dart'; class CourseDetailsModel extends BaseModel { - User student; + User? student; String courseId; // Could be routed to without a full course, only the id may be known - Course course; - CourseSettings courseSettings; - List tabs = List(); + Course? course; + CourseSettings? courseSettings; + List? tabs = []; bool forceRefresh = true; - GradingPeriod _currentGradingPeriod; - GradingPeriod _nextGradingPeriod; + GradingPeriod? _currentGradingPeriod; + GradingPeriod? _nextGradingPeriod; CourseDetailsModel(this.student, this.courseId); // A convenience constructor when we already have the course data - CourseDetailsModel.withCourse(this.student, this.course) : this.courseId = course.id; + CourseDetailsModel.withCourse(this.student, this.course) : this.courseId = course!.id; /// Used only be the skeleton to load the course data for creating tabs and the app bar Future loadData({bool refreshCourse = false}) { @@ -65,11 +66,11 @@ class CourseDetailsModel extends BaseModel { // Set the _nextGradingPeriod to the current enrollment period (if active and if not already set) final enrollment = - course?.enrollments?.firstWhere((enrollment) => enrollment.userId == student.id, orElse: () => null); + course?.enrollments?.firstWhereOrNull((enrollment) => enrollment.userId == student?.id); if (_nextGradingPeriod == null && enrollment?.hasActiveGradingPeriod() == true) { _nextGradingPeriod = GradingPeriod((b) => b - ..id = enrollment.currentGradingPeriodId - ..title = enrollment.currentGradingPeriodTitle); + ..id = enrollment?.currentGradingPeriodId + ..title = enrollment?.currentGradingPeriodTitle); } return Future.value(); }); @@ -81,25 +82,21 @@ class CourseDetailsModel extends BaseModel { } final groupFuture = _interactor() - .loadAssignmentGroups(courseId, student.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh) - ?.then((groups) async { + .loadAssignmentGroups(courseId, student?.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh).then((groups) async { // Remove unpublished assignments to match web - return groups - ?.map((group) => (group.toBuilder()..assignments.removeWhere((assignment) => !assignment.published)).build()) - ?.toList(); + return groups?.map((group) => (group.toBuilder()..assignments.removeWhere((assignment) => !assignment.published)).build()).toList(); }); final gradingPeriodsFuture = - _interactor().loadGradingPeriods(courseId, forceRefresh: forceRefresh)?.then((periods) { - return periods?.gradingPeriods?.toList() ?? []; + _interactor().loadGradingPeriods(courseId, forceRefresh: forceRefresh).then((periods) { + return periods?.gradingPeriods.toList(); }); // Get the grades for the term final enrollmentsFuture = _interactor() - .loadEnrollmentsForGradingPeriod(courseId, student.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh) - ?.then((enrollments) { - return enrollments.length > 0 ? enrollments.first : null; - })?.catchError((_) => null); // Some 'legacy' parents can't read grades for students, so catch and return null + .loadEnrollmentsForGradingPeriod(courseId, student?.id, _nextGradingPeriod?.id, forceRefresh: forceRefresh).then((enrollments) { + return enrollments != null && enrollments.length > 0 ? enrollments.first : null; + }).catchError((_) => null); // Some 'legacy' parents can't read grades for students, so catch and return null final gradeDetails = GradeDetails( assignmentGroups: await groupFuture, @@ -113,9 +110,9 @@ class CourseDetailsModel extends BaseModel { return gradeDetails; } - Future> loadSummary({bool refresh: false}) async { + Future?> loadSummary({bool refresh = false}) async { // Get all assignment and calendar events - List> results = await Future.wait([ + List?> results = await Future.wait([ _interactor().loadScheduleItems(courseId, ScheduleItem.apiTypeCalendar, refresh), _interactor().loadScheduleItems(courseId, ScheduleItem.apiTypeAssignment, refresh), ]); @@ -125,17 +122,17 @@ class CourseDetailsModel extends BaseModel { // directly in debug builds. if (kDebugMode) { return Future(() { - return processSummaryItems(Tuple2(results, student.id)); + return processSummaryItems(Tuple2(results, student?.id)); }); } else { // Potentially heavy list operations going on here, so we'll use a background isolate - return compute(processSummaryItems, Tuple2(results, student.id)); + return compute(processSummaryItems, Tuple2(results, student?.id)); } } @visibleForTesting - static List processSummaryItems(Tuple2>, String> input) { - var results = input.item1; + static List processSummaryItems(Tuple2?>, String?> input) { + var results = input.item1.nonNulls; var studentId = input.item2; // Flat map to a single list @@ -146,7 +143,7 @@ class CourseDetailsModel extends BaseModel { we only want to keep that one. If none of the overrides apply, we only want to keep the item with the base dates. */ var overrides = items.where((item) => item.assignmentOverrides != null).toList(); overrides.forEach((item) { - if (item.assignmentOverrides.any((it) => it.studentIds.contains(studentId))) { + if (item.assignmentOverrides?.any((it) => it.studentIds.contains(studentId)) == true) { // This item applies to the current student. Remove all other items that have the same ID. items.retainWhere((it) => it == item || it.id != item.id); } else { @@ -157,10 +154,10 @@ class CourseDetailsModel extends BaseModel { // Sort by ascending date, using a future date as a fallback so that undated items appear at the end // If dates match (which will be the case for undated items), then sort by title - return items.sortBy([ - (it) => it.startAt ?? it.allDayDate, - (it) => it.title, - ]); + return items.sortBySelector([ + (it) => it?.startAt ?? it?.allDayDate, + (it) => it?.title, + ])?.toList().nonNulls.toList() ?? []; } CourseDetailsInteractor _interactor() => locator(); @@ -176,26 +173,26 @@ class CourseDetailsModel extends BaseModel { bool get hasHomePageAsSyllabus => course?.syllabusBody?.isNotEmpty == true && (course?.homePage == HomePage.syllabus || - (course?.homePage != HomePage.wiki && tabs.any((tab) => tab.id == HomePage.syllabus.name))); + (course?.homePage != HomePage.wiki && tabs?.any((tab) => tab.id == HomePage.syllabus.name) == true)); bool get showSummary => hasHomePageAsSyllabus && (courseSettings?.courseSummary == true); bool get restrictQuantitativeData => courseSettings?.restrictQuantitativeData == true; - GradingPeriod currentGradingPeriod() => _currentGradingPeriod; + GradingPeriod? currentGradingPeriod() => _currentGradingPeriod; /// This sets the next grading period to use when loadAssignments is called. [currentGradingPeriod] won't be updated /// until the load call is finished, this way the grading period isn't updated in the ui until the rest of the data /// updates to reflect the new grading period. - updateGradingPeriod(GradingPeriod period) { + updateGradingPeriod(GradingPeriod? period) { _nextGradingPeriod = period; } } class GradeDetails { - final List assignmentGroups; - final List gradingPeriods; - final Enrollment termEnrollment; + final List? assignmentGroups; + final List? gradingPeriods; + final Enrollment? termEnrollment; GradeDetails({this.assignmentGroups, this.gradingPeriods, this.termEnrollment}); } diff --git a/apps/flutter_parent/lib/screens/courses/details/course_details_screen.dart b/apps/flutter_parent/lib/screens/courses/details/course_details_screen.dart index 005650dab6..0614d227d9 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_details_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_details_screen.dart @@ -35,21 +35,19 @@ import 'package:provider/provider.dart'; class CourseDetailsScreen extends StatefulWidget { final CourseDetailsModel _model; - CourseDetailsScreen(String courseId, {Key key}) - : this._model = CourseDetailsModel(ApiPrefs.getCurrentStudent(), courseId), - super(key: key); + CourseDetailsScreen(String courseId, {super.key}) + : this._model = CourseDetailsModel(ApiPrefs.getCurrentStudent(), courseId); // A convenience constructor when we already have the course data, so we don't load something we already have - CourseDetailsScreen.withCourse(Course course, {Key key}) - : this._model = CourseDetailsModel.withCourse(ApiPrefs.getCurrentStudent(), course), - super(key: key); + CourseDetailsScreen.withCourse(Course course, {super.key}) + : this._model = CourseDetailsModel.withCourse(ApiPrefs.getCurrentStudent()! , course); @override _CourseDetailsScreenState createState() => _CourseDetailsScreenState(); } class _CourseDetailsScreenState extends State with SingleTickerProviderStateMixin { - TabController _tabController; + TabController? _tabController; @override void initState() { @@ -100,7 +98,7 @@ class _CourseDetailsScreenState extends State with SingleTi ); } - Widget _appBar(BuildContext context, CourseDetailsModel model) { + AppBar _appBar(BuildContext context, CourseDetailsModel model) { final tabCount = model.tabCount(); return AppBar( title: Text(model.course?.name ?? ''), @@ -116,7 +114,7 @@ class _CourseDetailsScreenState extends State with SingleTi }, ), ], - bottom: ParentTheme.of(context).appBarDivider( + bottom: ParentTheme.of(context)?.appBarDivider( bottom: (tabCount <= 1) ? null // Don't show the tab bar if we only have one tab : TabBar( @@ -147,7 +145,7 @@ class _CourseDetailsScreenState extends State with SingleTi children: [ CourseGradesScreen(), if (model.hasHomePageAsFrontPage) CourseFrontPageScreen(courseId: model.courseId), - if (model.hasHomePageAsSyllabus) CourseSyllabusScreen(model.course.syllabusBody), + if (model.hasHomePageAsSyllabus) CourseSyllabusScreen(model.course!.syllabusBody!), if (model.showSummary) CourseSummaryScreen(), ], ); @@ -157,23 +155,23 @@ class _CourseDetailsScreenState extends State with SingleTi void _sendMessage(bool hasSyllabus) { String subject; String urlLink = '${ApiPrefs.getDomain()}/courses/${widget._model.courseId}'; - if (_tabController.index == 0) { + if (_tabController?.index == 0) { // Grades - subject = L10n(context).gradesSubjectMessage(widget._model.student.name); + subject = L10n(context).gradesSubjectMessage(widget._model.student?.name ?? ''); urlLink += '/grades'; } else if (hasSyllabus) { // Syllabus - subject = L10n(context).syllabusSubjectMessage(widget._model.student.name); + subject = L10n(context).syllabusSubjectMessage(widget._model.student?.name ?? ''); urlLink += '/assignments/syllabus'; } else { // Front Page - subject = L10n(context).frontPageSubjectMessage(widget._model.student.name); + subject = L10n(context).frontPageSubjectMessage(widget._model.student?.name ?? ''); } - String postscript = L10n(context).messageLinkPostscript(widget._model.student.name, urlLink); + String postscript = L10n(context).messageLinkPostscript(widget._model.student?.name ?? '', urlLink); Widget screen = CreateConversationScreen( widget._model.courseId, - widget._model.student.id, + widget._model.student?.id, subject, postscript, ); diff --git a/apps/flutter_parent/lib/screens/courses/details/course_front_page_screen.dart b/apps/flutter_parent/lib/screens/courses/details/course_front_page_screen.dart index dc2966189c..0b35844250 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_front_page_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_front_page_screen.dart @@ -11,7 +11,7 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:flutter/foundation.dart'; + import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/canvas_page.dart'; @@ -24,25 +24,23 @@ import 'package:flutter_parent/utils/service_locator.dart'; class CourseFrontPageScreen extends StatefulWidget { final String courseId; - CourseFrontPageScreen({Key key, this.courseId}) - : assert(courseId != null), - super(key: key); + CourseFrontPageScreen({required this.courseId, super.key}); @override _CourseFrontPageScreenState createState() => _CourseFrontPageScreenState(); } class _CourseFrontPageScreenState extends State with AutomaticKeepAliveClientMixin { - Future _pageFuture; + Future? _pageFuture; @override bool get wantKeepAlive => true; - Future _refreshPage() { + Future? _refreshPage() { setState(() { _pageFuture = _interactor.loadFrontPage(widget.courseId, forceRefresh: true); }); - return _pageFuture?.catchError((_) {}); + return _pageFuture?.catchError((_) { return Future.value(null); }); } CourseDetailsInteractor get _interactor => locator(); @@ -58,15 +56,15 @@ class _CourseFrontPageScreenState extends State with Auto super.build(context); // Required super call for AutomaticKeepAliveClientMixin return FutureBuilder( future: _pageFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ErrorPandaWidget(L10n(context).unexpectedError, () => _refreshPage()); } else if (!snapshot.hasData) { return LoadingIndicator(); } else { return CanvasWebView( - content: snapshot.data.body, - emptyDescription: snapshot.data.lockExplanation ?? L10n(context).noPageFound, + content: snapshot.data!.body!, + emptyDescription: snapshot.data!.lockExplanation ?? L10n(context).noPageFound, horizontalPadding: 16, ); } diff --git a/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart b/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart index 21ec29988d..97e7c9222e 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_grades_screen.dart @@ -42,8 +42,8 @@ class CourseGradesScreen extends StatefulWidget { } class _CourseGradesScreenState extends State with AutomaticKeepAliveClientMixin { - Set _collapsedGroupIds; - Future _detailsFuture; + late Set _collapsedGroupIds; + Future? _detailsFuture; static final GlobalKey _refreshIndicatorKey = new GlobalKey(); @override @@ -64,7 +64,7 @@ class _CourseGradesScreenState extends State with AutomaticK if (_detailsFuture == null) _detailsFuture = model.loadAssignments(); return RefreshIndicator( key: _refreshIndicatorKey, - onRefresh: () => _refresh(model), + onRefresh: () => _refresh(model)!, child: FutureBuilder( future: _detailsFuture, builder: (context, AsyncSnapshot snapshot) => _body(snapshot, model), @@ -74,12 +74,12 @@ class _CourseGradesScreenState extends State with AutomaticK ); } - Future _refresh(CourseDetailsModel model) { + Future? _refresh(CourseDetailsModel model) { setState(() { _detailsFuture = model.loadAssignments(forceRefresh: model.forceRefresh); model.forceRefresh = true; }); - return _detailsFuture.catchError((_) {}); + return _detailsFuture?.catchError((_) {}); } Widget _body(AsyncSnapshot snapshot, CourseDetailsModel model) { @@ -94,13 +94,14 @@ class _CourseGradesScreenState extends State with AutomaticK return ErrorPandaWidget( L10n(context).unexpectedError, () { - _refreshIndicatorKey.currentState.show(); + _refreshIndicatorKey.currentState?.show(); }, ); } else if (!snapshot.hasData || - snapshot.data.assignmentGroups == null || - snapshot.data.assignmentGroups.isEmpty || - snapshot.data.assignmentGroups.every((group) => group.assignments.isEmpty) == true) { + model.course == null || + snapshot.data?.assignmentGroups == null || + snapshot.data?.assignmentGroups?.isEmpty == true || + snapshot.data?.assignmentGroups?.every((group) => group.assignments.isEmpty) == true) { return EmptyPandaWidget( svgPath: 'assets/svg/panda-space-no-assignments.svg', title: L10n(context).noAssignmentsTitle, @@ -113,13 +114,13 @@ class _CourseGradesScreenState extends State with AutomaticK return ListView( children: [ header, - ..._assignmentListChildren(context, snapshot.data.assignmentGroups, model.course), + ..._assignmentListChildren(context, snapshot.data!.assignmentGroups!, model.course!), ], ); } List _assignmentListChildren(BuildContext context, List groups, Course course) { - final children = List(); + List children = []; for (AssignmentGroup group in groups) { if (group.assignments.length == 0) continue; // Don't show empty assignment groups @@ -138,13 +139,13 @@ class _CourseGradesScreenState extends State with AutomaticK }, title: Padding( padding: const EdgeInsetsDirectional.only(top: 16, start: 16, end: 16), - child: Text(group.name, style: Theme.of(context).textTheme.overline), + child: Text(group.name, style: Theme.of(context).textTheme.labelSmall), ), trailing: Padding( padding: const EdgeInsetsDirectional.only(top: 16, start: 16, end: 16), child: Icon( isCollapsed ? CanvasIcons.mini_arrow_down : CanvasIcons.mini_arrow_up, - color: Theme.of(context).textTheme.overline.color, + color: Theme.of(context).textTheme.labelSmall!.color, semanticLabel: isCollapsed ? L10n(context).allyCollapsed : L10n(context).allyExpanded, ), ), @@ -167,11 +168,10 @@ class _CourseGradesScreenState extends State with AutomaticK class _CourseGradeHeader extends StatelessWidget { final List gradingPeriods; - final Enrollment termEnrollment; + final Enrollment? termEnrollment; - _CourseGradeHeader(BuildContext context, List gradingPeriods, this.termEnrollment, {Key key}) - : this.gradingPeriods = [GradingPeriod((b) => b..title = L10n(context).allGradingPeriods)] + gradingPeriods, - super(key: key); + _CourseGradeHeader(BuildContext context, List gradingPeriods, this.termEnrollment, {super.key}) + : this.gradingPeriods = [GradingPeriod((b) => b..title = L10n(context).allGradingPeriods)] + gradingPeriods; @override Widget build(BuildContext context) { @@ -190,11 +190,11 @@ class _CourseGradeHeader extends StatelessWidget { ); } - Widget _gradingPeriodHeader(BuildContext context, CourseDetailsModel model) { + Widget? _gradingPeriodHeader(BuildContext context, CourseDetailsModel model) { // Don't show this if there's no grading periods (we always add 1 for 'All grading periods') if (gradingPeriods.length <= 1) return null; - final studentColor = ParentTheme.of(context).studentColor; + final studentColor = ParentTheme.of(context)?.studentColor; final gradingPeriod = model.currentGradingPeriod() ?? gradingPeriods.first; @@ -203,10 +203,9 @@ class _CourseGradeHeader extends StatelessWidget { padding: const EdgeInsetsDirectional.only(start: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.ideographic, children: [ - Text(gradingPeriod.title, style: Theme.of(context).textTheme.headline4), + Text(gradingPeriod.title!, style: Theme.of(context).textTheme.headlineMedium), InkWell( child: ConstrainedBox( constraints: BoxConstraints(minHeight: 48, minWidth: 48), // For a11y @@ -216,7 +215,7 @@ class _CourseGradeHeader extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text( L10n(context).filter, - style: Theme.of(context).textTheme.caption.copyWith(color: studentColor), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: studentColor), ), ), ), @@ -227,7 +226,7 @@ class _CourseGradeHeader extends StatelessWidget { // Don't force refresh when switching grading periods model.forceRefresh = false; - _CourseGradesScreenState._refreshIndicatorKey.currentState.show(); + _CourseGradesScreenState._refreshIndicatorKey.currentState?.show(); }, ), ], @@ -236,18 +235,18 @@ class _CourseGradeHeader extends StatelessWidget { } /// The total grade in the course/grading period - Widget _gradeTotal(BuildContext context, CourseDetailsModel model) { - final grade = model.course.getCourseGrade( - model.student.id, + Widget? _gradeTotal(BuildContext context, CourseDetailsModel model) { + final grade = model.course?.getCourseGrade( + model.student?.id, enrollment: termEnrollment, gradingPeriodId: model.currentGradingPeriod()?.id, forceAllPeriods: termEnrollment == null && model.currentGradingPeriod()?.id == null, ); // Don't show the total if the grade is locked - if (grade.isCourseGradeLocked(forAllGradingPeriods: model.currentGradingPeriod()?.id == null)) return null; + if (grade == null || grade.isCourseGradeLocked(forAllGradingPeriods: model.currentGradingPeriod()?.id == null)) return null; - if ((model.courseSettings?.restrictQuantitativeData ?? false) && (grade.currentGrade() == null || grade.currentGrade().isEmpty)) return null; + if ((model.courseSettings?.restrictQuantitativeData ?? false) && (grade.currentGrade() == null || grade.currentGrade()?.isEmpty == true)) return null; final textTheme = Theme.of(context).textTheme; return Padding( @@ -255,8 +254,8 @@ class _CourseGradeHeader extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(L10n(context).courseTotalGradeLabel, style: textTheme.bodyText2), - Text(_courseGrade(context, grade), style: textTheme.bodyText2, key: Key("total_grade")), + Text(L10n(context).courseTotalGradeLabel, style: textTheme.bodyMedium), + Text(_courseGrade(context, grade), style: textTheme.bodyMedium, key: Key("total_grade")), ], ), ); @@ -270,8 +269,8 @@ class _CourseGradeHeader extends StatelessWidget { return L10n(context).noGrade; } else { return grade.currentGrade()?.isNotEmpty == true - ? grade.currentGrade() - : format.format(grade.currentScore() / 100); // format multiplies by 100 for percentages + ? grade.currentGrade()! + : format.format(grade.currentScore()! / 100); // format multiplies by 100 for percentages } } } @@ -280,12 +279,12 @@ class _AssignmentRow extends StatelessWidget { final Assignment assignment; final Course course; - const _AssignmentRow({Key key, this.assignment, this.course}) : super(key: key); + const _AssignmentRow({required this.assignment, required this.course, super.key}); @override Widget build(BuildContext context) { final model = Provider.of(context, listen: false); - final studentId = model.student.id; + final studentId = model.student?.id; final textTheme = Theme.of(context).textTheme; final assignmentStatus = _assignmentStatus(context, assignment, studentId); @@ -302,7 +301,7 @@ class _AssignmentRow extends StatelessWidget { Container( padding: EdgeInsets.only(top: 4), width: 20, - child: Icon(CanvasIcons.assignment, size: 20, color: ParentTheme.of(context).studentColor), + child: Icon(CanvasIcons.assignment, size: 20, color: ParentTheme.of(context)?.studentColor), ), SizedBox(width: 32), Expanded( @@ -314,10 +313,10 @@ class _AssignmentRow extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(assignment.name, style: textTheme.subtitle1, key: Key("assignment_${assignment.id}_name")), + Text(assignment.name!, style: textTheme.titleMedium, key: Key("assignment_${assignment.id}_name")), SizedBox(height: 2), Text(_formatDate(context, assignment.dueAt), - style: textTheme.caption, key: Key("assignment_${assignment.id}_dueAt")), + style: textTheme.bodySmall, key: Key("assignment_${assignment.id}_dueAt")), if (assignmentStatus != null) SizedBox(height: 4), if (assignmentStatus != null) assignmentStatus, ], @@ -334,7 +333,7 @@ class _AssignmentRow extends StatelessWidget { ); } - Widget _assignmentStatus(BuildContext context, Assignment assignment, String studentId) { + Widget? _assignmentStatus(BuildContext context, Assignment assignment, String? studentId) { final localizations = L10n(context); final textTheme = Theme.of(context).textTheme; final status = assignment.getStatus(studentId: studentId); @@ -344,24 +343,24 @@ class _AssignmentRow extends StatelessWidget { return null; // An 'invisible' status, just don't show anything case SubmissionStatus.LATE: return Text(localizations.assignmentLateSubmittedLabel, - style: textTheme.caption.copyWith( + style: textTheme.bodySmall?.copyWith( // Late will be orange, regardless of the current student - color: ParentTheme.of(context).getColorVariantForCurrentState(StudentColorSet.fire), + color: ParentTheme.of(context)?.getColorVariantForCurrentState(StudentColorSet.fire), ), key: key); case SubmissionStatus.MISSING: return Text(localizations.assignmentMissingSubmittedLabel, - style: textTheme.caption.copyWith(color: ParentColors.failure), key: key); + style: textTheme.bodySmall?.copyWith(color: ParentColors.failure), key: key); case SubmissionStatus.SUBMITTED: - return Text(localizations.assignmentSubmittedLabel, style: textTheme.caption, key: key); + return Text(localizations.assignmentSubmittedLabel, style: textTheme.bodySmall, key: key); case SubmissionStatus.NOT_SUBMITTED: - return Text(localizations.assignmentNotSubmittedLabel, style: textTheme.caption, key: key); + return Text(localizations.assignmentNotSubmittedLabel, style: textTheme.bodySmall, key: key); default: return null; } } - Widget _assignmentGrade(BuildContext context, Assignment assignment, String studentId) { + Widget _assignmentGrade(BuildContext context, Assignment assignment, String? studentId) { dynamic points = assignment.pointsPossible; // Store the points as an int if possible @@ -387,8 +386,8 @@ class _AssignmentRow extends StatelessWidget { : localizations.contentDescriptionScoreOutOfPointsPossible(localizations.excused, points); } else if (submission?.grade != null) { String grade = restrictQuantitativeData && assignment.isGradingTypeQuantitative() - ? course.convertScoreToLetterGrade(submission.score, assignment.pointsPossible) - : submission.grade; + ? course.convertScoreToLetterGrade(submission!.score, assignment.pointsPossible) + : submission!.grade!; text = restrictQuantitativeData ? grade : localizations.gradeFormatScoreOutOfPointsPossible(grade, points); @@ -406,11 +405,11 @@ class _AssignmentRow extends StatelessWidget { return Text(text, semanticsLabel: semantics, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, key: Key("assignment_${assignment.id}_grade")); } - String _formatDate(BuildContext context, DateTime date) { + String _formatDate(BuildContext context, DateTime? date) { final l10n = L10n(context); return date.l10nFormat(l10n.dueDateAtTime) ?? l10n.noDueDate; } diff --git a/apps/flutter_parent/lib/screens/courses/details/course_summary_screen.dart b/apps/flutter_parent/lib/screens/courses/details/course_summary_screen.dart index 1822494f3d..ca1fbe174e 100644 --- a/apps/flutter_parent/lib/screens/courses/details/course_summary_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/details/course_summary_screen.dart @@ -38,7 +38,7 @@ class CourseSummaryScreen extends StatelessWidget { class _CourseSummary extends StatefulWidget { final CourseDetailsModel model; - const _CourseSummary(this.model, {Key key}) : super(key: key); + const _CourseSummary(this.model, {super.key}); @override __CourseSummaryState createState() => __CourseSummaryState(); @@ -46,7 +46,7 @@ class _CourseSummary extends StatefulWidget { class __CourseSummaryState extends State<_CourseSummary> with AutomaticKeepAliveClientMixin { GlobalKey _refreshKey = GlobalKey(); - Future> _future; + late Future?> _future; @override bool get wantKeepAlive => true; // Retain this screen's state when switching tabs @@ -61,7 +61,7 @@ class __CourseSummaryState extends State<_CourseSummary> with AutomaticKeepAlive setState(() { _future = widget.model.loadSummary(refresh: true); }); - return _future.catchError((_) {}); + _future.catchError((_) { return Future.value(null); }); } @override @@ -72,13 +72,13 @@ class __CourseSummaryState extends State<_CourseSummary> with AutomaticKeepAlive onRefresh: _refresh, child: FutureBuilder( future: _future, - builder: (BuildContext context, AsyncSnapshot> snapshot) { + builder: (BuildContext context, AsyncSnapshot?> snapshot) { if (snapshot.connectionState == ConnectionState.waiting && !snapshot.hasData) { return LoadingIndicator(); - } else if (snapshot.hasError) { + } else if (snapshot.hasError || !snapshot.hasData) { return ErrorPandaWidget(L10n(context).errorLoadingCourseSummary, _refresh); } else { - return _body(snapshot.data); + return _body(snapshot.data!); } }, ), @@ -108,25 +108,25 @@ class __CourseSummaryState extends State<_CourseSummary> with AutomaticKeepAlive if (date == null) { dateText = L10n(context).noDueDate; } else { - dateText = date.l10nFormat(L10n(context).dateAtTime); + dateText = date.l10nFormat(L10n(context).dateAtTime)!; } // Compute itemId for use with key values, which are used for testing - var itemId = item.id; + String? itemId = item.id; if (item.type == ScheduleItem.apiTypeAssignment && item.assignment != null) { - itemId = item.assignment.isQuiz ? item.assignment.quizId : item.assignment.id; + itemId = item.assignment!.isQuiz ? item.assignment!.quizId : item.assignment!.id; } return ListTile( - title: Text(item.title, key: ValueKey('summary_item_title_$itemId')), + title: Text(item.title!, key: ValueKey('summary_item_title_$itemId')), subtitle: Text(dateText, key: ValueKey('summary_item_subtitle_$itemId')), - leading: Icon(_getIcon(item), color: Theme.of(context).accentColor, key: ValueKey('summary_item_icon_$itemId')), + leading: Icon(_getIcon(item), color: Theme.of(context).colorScheme.secondary, key: ValueKey('summary_item_icon_$itemId')), onTap: () { if (item.type == ScheduleItem.apiTypeCalendar) { locator().pushRoute(context, PandaRouter.eventDetails(widget.model.courseId, item.id)); } else { locator() - .pushRoute(context, PandaRouter.assignmentDetails(widget.model.courseId, item.assignment.id)); + .pushRoute(context, PandaRouter.assignmentDetails(widget.model.courseId, item.assignment!.id)); } }, ); diff --git a/apps/flutter_parent/lib/screens/courses/details/grading_period_modal.dart b/apps/flutter_parent/lib/screens/courses/details/grading_period_modal.dart index a836c5e72e..d09769c287 100644 --- a/apps/flutter_parent/lib/screens/courses/details/grading_period_modal.dart +++ b/apps/flutter_parent/lib/screens/courses/details/grading_period_modal.dart @@ -18,9 +18,9 @@ import 'package:flutter_parent/models/grading_period.dart'; class GradingPeriodModal extends StatelessWidget { final List gradingPeriods; - const GradingPeriodModal._internal({Key key, this.gradingPeriods}) : super(key: key); + const GradingPeriodModal._internal({required this.gradingPeriods, super.key}); - static Future asBottomSheet(BuildContext context, List gradingPeriods) => + static Future asBottomSheet(BuildContext context, List gradingPeriods) => showModalBottomSheet( context: context, builder: (context) => GradingPeriodModal._internal(gradingPeriods: gradingPeriods), @@ -35,12 +35,12 @@ class GradingPeriodModal extends StatelessWidget { if (index == 0) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Text(L10n(context).filterBy, style: Theme.of(context).textTheme.caption), + child: Text(L10n(context).filterBy, style: Theme.of(context).textTheme.bodySmall), ); } final gradingPeriod = gradingPeriods[index - 1]; return ListTile( - title: Text(gradingPeriod.title, style: Theme.of(context).textTheme.subtitle1), + title: Text(gradingPeriod.title!, style: Theme.of(context).textTheme.titleMedium), onTap: () => Navigator.of(context).pop(gradingPeriod), ); }, diff --git a/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_interactor.dart b/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_interactor.dart index f1bf254dd6..5de0b6c682 100644 --- a/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_interactor.dart +++ b/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_interactor.dart @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +import 'package:flutter/cupertino.dart'; import 'package:flutter_parent/models/canvas_page.dart'; import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/network/api/course_api.dart'; @@ -22,9 +23,9 @@ import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shel import 'package:flutter_parent/utils/service_locator.dart'; class CourseRoutingShellInteractor { - Future loadCourseShell(CourseShellType type, String courseId, {bool forceRefresh = false}) async { + Future loadCourseShell(CourseShellType type, String courseId, {bool forceRefresh = false}) async { var course = await _loadCourse(courseId, forceRefresh: forceRefresh); - CanvasPage frontPage = null; + CanvasPage? frontPage = null; if (type == CourseShellType.frontPage) { frontPage = await _loadHomePage(courseId, forceRefresh: forceRefresh); @@ -33,25 +34,25 @@ class CourseRoutingShellInteractor { } } - if (type == CourseShellType.syllabus && course.syllabusBody == null) { + if (type == CourseShellType.syllabus && course?.syllabusBody == null) { return Future.error(''); } return CourseShellData(course, frontPage: frontPage); } - Future _loadCourse(String courseId, {bool forceRefresh = false}) { + Future _loadCourse(String courseId, {bool forceRefresh = false}) { return locator().getCourse(courseId, forceRefresh: forceRefresh); } - Future _loadHomePage(String courseId, {bool forceRefresh = false}) { + Future _loadHomePage(String courseId, {bool forceRefresh = false}) { return locator().getCourseFrontPage(courseId, forceRefresh: forceRefresh); } } class CourseShellData { - final Course course; - final CanvasPage frontPage; + final Course? course; + final CanvasPage? frontPage; CourseShellData(this.course, {this.frontPage}); } diff --git a/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_screen.dart b/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_screen.dart index 735f12d37e..4a0f388f13 100644 --- a/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_screen.dart +++ b/apps/flutter_parent/lib/screens/courses/routing_shell/course_routing_shell_screen.dart @@ -33,21 +33,21 @@ class CourseRoutingShellScreen extends StatefulWidget { final String courseId; final CourseShellType type; - CourseRoutingShellScreen(this.courseId, this.type, {Key key}) : super(key: key); + CourseRoutingShellScreen(this.courseId, this.type, {super.key}); @override State createState() => _CourseRoutingShellScreenState(); } class _CourseRoutingShellScreenState extends State { - Future _dataFuture; + Future? _dataFuture; - Future _refresh() { + Future? _refresh() { setState(() { _dataFuture = locator().loadCourseShell(widget.type, widget.courseId, forceRefresh: true); }); - return _dataFuture?.catchError((_) {}); + return _dataFuture?.catchError((_) { return Future.value(null); }); } @override @@ -58,7 +58,7 @@ class _CourseRoutingShellScreenState extends State { return FutureBuilder( future: _dataFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container(color: Theme.of(context).scaffoldBackgroundColor, child: LoadingIndicator()); } @@ -66,7 +66,7 @@ class _CourseRoutingShellScreenState extends State { if (snapshot.hasError || snapshot.data == null) { return _error(); } else { - return _scaffold(widget.type, snapshot.data); + return _scaffold(widget.type, snapshot.data!); } }, ); @@ -88,19 +88,19 @@ class _CourseRoutingShellScreenState extends State { (widget.type == CourseShellType.frontPage) ? L10n(context).courseFrontPageLabel.toUpperCase() : L10n(context).courseSyllabusLabel.toUpperCase(), - data.course.name), - bottom: ParentTheme.of(context).appBarDivider(), + data.course?.name ?? ''), + bottom: ParentTheme.of(context)?.appBarDivider(), ), body: _body(data)); } Widget _body(CourseShellData data) { return widget.type == CourseShellType.frontPage - ? _webView(data.frontPage.body, emptyDescription: data.frontPage.lockExplanation ?? L10n(context).noPageFound) - : _webView(data.course.syllabusBody); + ? _webView(data.frontPage!.body!, emptyDescription: data.frontPage?.lockExplanation ?? L10n(context).noPageFound) + : _webView(data.course?.syllabusBody ?? ''); } - Widget _webView(String html, {String emptyDescription}) { + Widget _webView(String html, {String? emptyDescription}) { return Padding( padding: const EdgeInsets.only(top: 16.0), child: CanvasWebView( @@ -117,7 +117,7 @@ class _CourseRoutingShellScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title), - Text(subtitle, style: Theme.of(context).primaryTextTheme.caption), + Text(subtitle, style: Theme.of(context).primaryTextTheme.bodySmall), ], ); } diff --git a/apps/flutter_parent/lib/screens/crash_screen.dart b/apps/flutter_parent/lib/screens/crash_screen.dart index 6be498fe83..e11ce87301 100644 --- a/apps/flutter_parent/lib/screens/crash_screen.dart +++ b/apps/flutter_parent/lib/screens/crash_screen.dart @@ -12,21 +12,20 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:device_info/device_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/utils/common_widgets/error_report/error_report_dialog.dart'; import 'package:flutter_parent/utils/common_widgets/respawn.dart'; import 'package:flutter_parent/utils/design/parent_colors.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class CrashScreen extends StatelessWidget { final FlutterErrorDetails error; - const CrashScreen(this.error, {Key key}) : super(key: key); + const CrashScreen(this.error, {super.key}); @override Widget build(BuildContext context) { @@ -36,7 +35,7 @@ class CrashScreen extends StatelessWidget { elevation: 0, backgroundColor: Colors.transparent, iconTheme: Theme.of(context).iconTheme, - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ) : null, body: _body(context), @@ -72,15 +71,17 @@ class CrashScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - FlatButton( + TextButton( onPressed: () => ErrorReportDialog.asDialog(context, error: error), child: Text( L10n(context).crashScreenContact, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16), + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16), ), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(4), - side: BorderSide(color: ParentColors.tiara), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(4), + side: BorderSide(color: ParentColors.tiara), + ), ), ), ], @@ -97,25 +98,25 @@ class CrashScreen extends StatelessWidget { future: Future.wait([PackageInfo.fromPlatform(), DeviceInfoPlugin().androidInfo]), builder: (context, snapshot) { if (!snapshot.hasData) return Container(); - PackageInfo packageInfo = snapshot.data[0]; - AndroidDeviceInfo deviceInfo = snapshot.data[1]; - return FlatButton( + PackageInfo packageInfo = snapshot.data![0] as PackageInfo; + AndroidDeviceInfo deviceInfo = snapshot.data![1] as AndroidDeviceInfo; + return TextButton( onPressed: () => _showDetailsDialog(context, packageInfo, deviceInfo), child: Text( L10n(context).crashScreenViewDetails, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleSmall, ), ); }, ); } - FlatButton _restartButton(BuildContext context) { - return FlatButton( - onPressed: () => Respawn.of(context).restart(), + TextButton _restartButton(BuildContext context) { + return TextButton( + onPressed: () => Respawn.of(context)?.restart(), child: Text( L10n(context).crashScreenRestart, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleSmall, ), ); } @@ -162,7 +163,7 @@ class CrashScreen extends StatelessWidget { key: Key('full-error-message'), padding: EdgeInsets.all(8), decoration: BoxDecoration( - color: ParentTheme.of(context).nearSurfaceColor, + color: ParentTheme.of(context)?.nearSurfaceColor, borderRadius: BorderRadius.all(Radius.circular(8))), child: Text( _getFullErrorMessage(), @@ -176,7 +177,7 @@ class CrashScreen extends StatelessWidget { ), ), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).done), onPressed: () => Navigator.of(context).pop(), ), diff --git a/apps/flutter_parent/lib/screens/dashboard/alert_notifier.dart b/apps/flutter_parent/lib/screens/dashboard/alert_notifier.dart index 13effba602..9090b82e9e 100644 --- a/apps/flutter_parent/lib/screens/dashboard/alert_notifier.dart +++ b/apps/flutter_parent/lib/screens/dashboard/alert_notifier.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_parent/models/alert.dart'; import 'package:flutter_parent/network/api/alert_api.dart'; import 'package:flutter_parent/utils/alert_helper.dart'; +import 'package:flutter_parent/utils/core_extensions/list_extensions.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class AlertCountNotifier extends ValueNotifier { @@ -23,10 +24,10 @@ class AlertCountNotifier extends ValueNotifier { update(String studentId) async { try { - final unreadAlerts = await locator().getAlertsDepaginated(studentId, true)?.then((List list) async { - return await locator().filterAlerts(list.where((element) => element.workflowState == AlertWorkflowState.unread).toList()); + final unreadAlerts = await locator().getAlertsDepaginated(studentId, true).then((List? list) async { + return await locator().filterAlerts(list?.where((element) => element.workflowState == AlertWorkflowState.unread).toList()); }); - value = unreadAlerts.length; + if (unreadAlerts != null) value = unreadAlerts.length; } catch (e) { print(e); } diff --git a/apps/flutter_parent/lib/screens/dashboard/dashboard_interactor.dart b/apps/flutter_parent/lib/screens/dashboard/dashboard_interactor.dart index 55869c76d2..98360d17a4 100644 --- a/apps/flutter_parent/lib/screens/dashboard/dashboard_interactor.dart +++ b/apps/flutter_parent/lib/screens/dashboard/dashboard_interactor.dart @@ -20,31 +20,35 @@ import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/screens/dashboard/alert_notifier.dart'; import 'package:flutter_parent/screens/dashboard/inbox_notifier.dart'; import 'package:flutter_parent/utils/old_app_migration.dart'; +import 'package:flutter_parent/utils/permission_handler.dart'; import 'package:flutter_parent/utils/service_locator.dart'; +import 'package:permission_handler/permission_handler.dart'; class DashboardInteractor { - Future> getStudents({bool forceRefresh = false}) async => + Future?> getStudents({bool forceRefresh = false}) async => locator().getObserveeEnrollments(forceRefresh: forceRefresh).then>((enrollments) { - List users = filterStudents(enrollments); + List? users = filterStudents(enrollments); sortUsers(users); - return users; + return Future.value(users); }); - Future getSelf({app}) async => locator().getSelf().then((user) async { - final permissions = (await locator().getSelfPermissions().catchError((_) => null)); - user = user.rebuild((b) => b..permissions = permissions?.toBuilder()); - ApiPrefs.setUser(user, app: app); + Future getSelf({app}) async => locator().getSelf().then((user) async { + UserPermission? permissions = (await locator().getSelfPermissions().catchError((_) => null)); + user = user?.rebuild((b) => b..permissions = permissions?.toBuilder()); + if (user != null) ApiPrefs.setUser(user, app: app); return user; }); - List filterStudents(List enrollments) => - enrollments.map((enrollment) => enrollment.observedUser).where((student) => student != null).toSet().toList(); + List? filterStudents(List? enrollments) => + enrollments?.map((enrollment) => enrollment.observedUser).nonNulls.toSet().toList(); - void sortUsers(List users) => users.sort((user1, user2) => user1.sortableName.compareTo(user2.sortableName)); + void sortUsers(List? users) => users?.sort((user1, user2) => user1.sortableName!.compareTo(user2.sortableName!)); InboxCountNotifier getInboxCountNotifier() => locator(); AlertCountNotifier getAlertCountNotifier() => locator(); - Future shouldShowOldReminderMessage() => locator().hasOldReminders(); + Future shouldShowOldReminderMessage() => locator().hasOldReminders(); + + Future requestNotificationPermission() => locator().requestPermission(Permission.notification); } diff --git a/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart b/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart index bf93b704da..02787a24d2 100644 --- a/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart +++ b/apps/flutter_parent/lib/screens/dashboard/dashboard_screen.dart @@ -46,31 +46,32 @@ import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'dashboard_interactor.dart'; class DashboardScreen extends StatefulWidget { - DashboardScreen({Key key, this.students, this.startingPage, this.deepLinkParams}) : super(key: key); + DashboardScreen({this.students, this.startingPage, this.deepLinkParams, super.key}); - final List students; + final List? students; // Used when deep linking into the courses, calendar, or alert screen - final DashboardContentScreens startingPage; - final Map deepLinkParams; + final DashboardContentScreens? startingPage; + final Map? deepLinkParams; @override State createState() => DashboardState(); } class DashboardState extends State { - GlobalKey scaffoldKey; + late GlobalKey scaffoldKey; DashboardInteractor _interactor = locator(); // Dashboard State List _students = []; - User _self; + late User? _self; bool _studentsLoading = false; bool _selfLoading = false; @@ -79,18 +80,18 @@ class DashboardState extends State { // ignore: unused_field bool _studentsError = false; - User _selectedStudent; - DashboardContentScreens _currentIndex; + User? _selectedStudent; + late DashboardContentScreens _currentIndex; bool expand = false; - SelectedStudentNotifier _selectedStudentNotifier; - CalendarTodayNotifier _showTodayNotifier; + late SelectedStudentNotifier _selectedStudentNotifier; + late CalendarTodayNotifier _showTodayNotifier; @visibleForTesting - Map currentDeepLinkParams; + Map? currentDeepLinkParams; - Function() _onStudentAdded; + late Function() _onStudentAdded; @override void initState() { @@ -101,13 +102,13 @@ class DashboardState extends State { _showTodayNotifier = CalendarTodayNotifier(); _loadSelf(); if (widget.students?.isNotEmpty == true) { - _students = widget.students; - String selectedStudentId = ApiPrefs.getCurrentLogin()?.selectedStudentId; + _students = widget.students!; + String? selectedStudentId = ApiPrefs.getCurrentLogin()?.selectedStudentId; _selectedStudent = _students.firstWhere((it) => it.id == selectedStudentId, orElse: () => _students.first); - _updateStudentColor(_selectedStudent.id); - _selectedStudentNotifier.value = _selectedStudent; + _updateStudentColor(_selectedStudent!.id); + _selectedStudentNotifier.value = _selectedStudent!; ApiPrefs.setCurrentStudent(_selectedStudent); - _interactor.getAlertCountNotifier().update(_selectedStudent.id); + _interactor.getAlertCountNotifier().update(_selectedStudent!.id); } else { _loadStudents(); } @@ -122,6 +123,8 @@ class DashboardState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { RatingDialog.asDialog(context); }); + + _interactor.requestNotificationPermission(); } @override @@ -132,7 +135,7 @@ class DashboardState extends State { void _updateStudentColor(String studentId) { WidgetsBinding.instance.scheduleFrameCallback((_) { - ParentTheme.of(context).setSelectedStudent(studentId); + ParentTheme.of(context)?.setSelectedStudent(studentId); }); } @@ -143,7 +146,7 @@ class DashboardState extends State { }); _interactor.getSelf(app: ParentApp.of(context)).then((user) { - _self = user; + _self = user!; setState(() { _selfLoading = false; }); @@ -162,16 +165,13 @@ class DashboardState extends State { }); _interactor.getStudents().then((users) { - _students = users; + _students = users!; if (_selectedStudent == null && _students.isNotEmpty) { setState(() { - String selectedStudentId = ApiPrefs.getCurrentLogin()?.selectedStudentId; + String? selectedStudentId = ApiPrefs.getCurrentLogin()?.selectedStudentId; _selectedStudent = _students.firstWhere((it) => it.id == selectedStudentId, orElse: () => _students.first); - _selectedStudentNotifier.value = _selectedStudent; - _updateStudentColor(_selectedStudent.id); - ApiPrefs.setCurrentStudent(_selectedStudent); - _interactor.getAlertCountNotifier().update(_selectedStudent.id); + updateStudent(); }); } @@ -196,7 +196,17 @@ class DashboardState extends State { _interactor.getStudents(forceRefresh: true).then((users) { setState(() { print('users: $users'); - _students = users; + + if (users != null && users.length > _students.length) { + var newStudents = users.toSet().difference(_students.toSet()); + _selectedStudent = newStudents.first; + updateStudent(); + } + _students = users!; + if (!users.map((e) => e.id).contains(_selectedStudent?.id)){ + _selectedStudent = _students.first; + updateStudent(); + } _studentsLoading = false; }); }).catchError((error) { @@ -208,6 +218,13 @@ class DashboardState extends State { }); } + void updateStudent() { + _selectedStudentNotifier.value = _selectedStudent!; + _updateStudentColor(_selectedStudent!.id); + ApiPrefs.setCurrentStudent(_selectedStudent); + _interactor.getAlertCountNotifier().update(_selectedStudent!.id); + } + @override Widget build(BuildContext context) { if (_currentIndex != DashboardContentScreens.Calendar) { @@ -253,24 +270,26 @@ class DashboardState extends State { child: _appBarStudents(_students, model.value), ), centerTitle: true, - bottom: ParentTheme.of(context).appBarDivider(), + bottom: ParentTheme.of(context)?.appBarDivider(), leading: IconButton( icon: WidgetBadge( Icon( Icons.menu, - color: Theme.of(context).primaryIconTheme.color, key: Key("drawer_menu"), ), countListenable: _interactor.getInboxCountNotifier(), options: BadgeOptions(includeBorder: true, onPrimarySurface: true), ), - onPressed: () => scaffoldKey.currentState.openDrawer(), + onPressed: () => scaffoldKey.currentState?.openDrawer(), tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, ), ), ), - drawer: Drawer( - child: SafeArea(child: _navDrawer(_self)), + drawer: SafeArea( + child: Drawer( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + child: SafeArea(child: _navDrawer(_self)), + ), ), body: Column(children: [ StudentExpansionWidget( @@ -291,7 +310,7 @@ class DashboardState extends State { preferredSize: Size.fromHeight(1), child: Divider( height: 1, - color: ParentTheme.of(context).isDarkMode + color: ParentTheme.of(context)?.isDarkMode == true ? ParentColors.oxford : ParentColors.appBarDividerLight), ), @@ -300,13 +319,13 @@ class DashboardState extends State { ), Expanded(child: _currentPage()) ]), - bottomNavigationBar: ParentTheme.of(context).bottomNavigationDivider( + bottomNavigationBar: ParentTheme.of(context)?.bottomNavigationDivider( _students.isEmpty ? Container() : BottomNavigationBar( - unselectedItemColor: ParentTheme.of(context).onSurfaceColor, - selectedFontSize: 10, - unselectedFontSize: 10, + unselectedItemColor: ParentTheme.of(context)?.onSurfaceColor, + selectedFontSize: 12, + unselectedFontSize: 12, backgroundColor: Theme.of(context).scaffoldBackgroundColor, items: _bottomNavigationBarItems(), currentIndex: this._currentIndex.index, @@ -319,7 +338,7 @@ class DashboardState extends State { ); } - Widget _appBarStudents(List students, User selectedStudent) { + Widget _appBarStudents(List students, User? selectedStudent) { if (students.isEmpty) { // No students yet, we are either still loading, or there was an error if (_studentsLoading) { @@ -330,7 +349,6 @@ class DashboardState extends State { return Center( child: Text( L10n(context).noStudents, - style: Theme.of(context).primaryTextTheme.headline6, ), ); } @@ -344,16 +362,18 @@ class DashboardState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Avatar(selectedStudent.avatarUrl, - name: selectedStudent.shortName, radius: 24, key: Key("student_expansion_touch_target")), + Avatar(selectedStudent?.avatarUrl, + name: selectedStudent?.shortName ?? '', + radius: 24, + key: Key("student_expansion_touch_target")), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - UserName.fromUserShortName(selectedStudent, style: Theme.of(context).primaryTextTheme.subtitle1), + UserName.fromUserShortName(selectedStudent!, style: TextStyle(color: Theme.of(context).primaryIconTheme.color ?? Colors.white)), SizedBox(width: 6), - DropdownArrow(rotate: expand), + DropdownArrow(rotate: expand, color: Theme.of(context).primaryIconTheme.color ?? Colors.white), ], ) ], @@ -375,10 +395,7 @@ class DashboardState extends State { light: 'assets/svg/bottom-nav/courses-light-selected.svg', dark: 'assets/svg/bottom-nav/courses-dark-selected.svg', ), - title: Padding( - padding: EdgeInsets.only(top: 4), - child: Text(L10n(context).coursesLabel), - ), + label: L10n(context).coursesLabel, ), BottomNavigationBarItem( icon: _navBarIcon( @@ -390,10 +407,7 @@ class DashboardState extends State { light: 'assets/svg/bottom-nav/calendar-light-selected.svg', dark: 'assets/svg/bottom-nav/calendar-dark-selected.svg', ), - title: Padding( - padding: EdgeInsets.only(top: 4), - child: Text(L10n(context).calendarLabel), - ), + label: L10n(context).calendarLabel, ), BottomNavigationBarItem( icon: WidgetBadge( @@ -415,48 +429,48 @@ class DashboardState extends State { options: BadgeOptions(includeBorder: true), key: Key('alerts-count'), ), - title: Padding( - padding: EdgeInsets.only(top: 4), - child: Text(L10n(context).alertsLabel), - ), + label: L10n(context).alertsLabel ), ]; } - Widget _navBarIcon({@required String light, @required String dark, bool active: false}) { - bool darkMode = ParentTheme.of(context).isDarkMode; + Widget _navBarIcon({required String light, required String dark, bool active = false}) { + bool darkMode = ParentTheme.of(context)?.isDarkMode ?? false; return SvgPicture.asset( darkMode ? dark : light, - color: active? ParentTheme.of(context).studentColor : null, + color: active? ParentTheme.of(context)?.studentColor : null, width: 24, height: 24, ); } - Widget _navDrawer(User user) { + Widget _navDrawer(User? user) { if (_selfLoading) { // Still loading... return LoadingIndicator(); } - return ListTileTheme( - style: ListTileStyle.list, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header - _navDrawerHeader(user), - Divider(), - // Tiles (Inbox, Manage Students, Sign Out, etc) - Expanded( - child: _navDrawerItemsList(), - ), + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: ListTileTheme( + style: ListTileStyle.list, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Header + _navDrawerHeader(user), + Divider(), + // Tiles (Inbox, Manage Students, Sign Out, etc) + Expanded( + child: _navDrawerItemsList(), + ), - // App version - if (ApiPrefs.getCurrentLogin()?.canMasquerade == true && !ApiPrefs.isMasquerading()) - _navDrawerActAsUser(), - if (ApiPrefs.isMasquerading()) - _navDrawerStopActingAsUser(), - _navDrawerAppVersion(), - ]), + // App version + if (ApiPrefs.getCurrentLogin()?.canMasquerade == true && !ApiPrefs.isMasquerading()) + _navDrawerActAsUser(), + if (ApiPrefs.isMasquerading()) + _navDrawerStopActingAsUser(), + _navDrawerAppVersion(), + ]), + ), ); } @@ -486,13 +500,13 @@ class DashboardState extends State { case DashboardContentScreens.Calendar: _page = CalendarScreen( startDate: currentDeepLinkParams != null - ? (currentDeepLinkParams.containsKey(CalendarScreen.startDateKey) - ? currentDeepLinkParams[CalendarScreen.startDateKey] as DateTime + ? (currentDeepLinkParams?.containsKey(CalendarScreen.startDateKey) == true + ? currentDeepLinkParams![CalendarScreen.startDateKey] as DateTime? : null) : null, startView: currentDeepLinkParams != null - ? (currentDeepLinkParams.containsKey(CalendarScreen.startViewKey) - ? currentDeepLinkParams[CalendarScreen.startViewKey] as CalendarView + ? (currentDeepLinkParams?.containsKey(CalendarScreen.startViewKey) == true + ? currentDeepLinkParams![CalendarScreen.startViewKey] as CalendarView : null) : null, ); @@ -513,7 +527,7 @@ class DashboardState extends State { } _showOldReminderMessage() async { - if (await _interactor.shouldShowOldReminderMessage()) { + if ((await _interactor.shouldShowOldReminderMessage()) == true) { WidgetsBinding.instance.addPostFrameCallback((_) { showDialog( context: context, @@ -523,7 +537,7 @@ class DashboardState extends State { title: Text(L10n(context).oldReminderMessageTitle), content: Text(L10n(context).oldReminderMessage), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).ok), onPressed: () { locator().logEvent(AnalyticsEventConstants.VIEWED_OLD_REMINDER_MESSAGE); @@ -546,10 +560,8 @@ class DashboardState extends State { _navigateToManageStudents(context) async { // Close the drawer, then push the Manage Children screen in Navigator.of(context).pop(); - var _addedStudentFuture = await locator().push(context, ManageStudentsScreen(_students)); - if (_addedStudentFuture) { - _addStudent(); - } + await locator().push(context, ManageStudentsScreen(_students)); + _addStudent(); } _navigateToSettings(context) { @@ -566,11 +578,10 @@ class DashboardState extends State { _performLogOut(BuildContext context, {bool switchingUsers = false}) async { try { - await ParentTheme.of(context).setSelectedStudent(null); - await locator() - .logEvent(switchingUsers ? AnalyticsEventConstants.SWITCH_USERS : AnalyticsEventConstants.LOGOUT); + await ParentTheme.of(context)?.setSelectedStudent(null); + locator().logEvent(switchingUsers ? AnalyticsEventConstants.SWITCH_USERS : AnalyticsEventConstants.LOGOUT); await ApiPrefs.performLogout(switchingLogins: switchingUsers, app: ParentApp.of(context)); - MasqueradeUI.of(context).refresh(); + MasqueradeUI.of(context)?.refresh(); locator().pushRouteAndClearStack(context, PandaRouter.login()); await FeaturesUtils.performLogout(); } catch (e) { @@ -579,22 +590,22 @@ class DashboardState extends State { } } - _navDrawerHeader(User user) => Column( + _navDrawerHeader(User? user) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 16, 0, 8), - child: Avatar(user.avatarUrl, name: user.shortName, radius: 40), + child: Avatar(user?.avatarUrl, name: user?.shortName, radius: 40), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: UserName.fromUser(user, style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), + child: UserName.fromUser(user!, style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), ), Padding( padding: const EdgeInsets.fromLTRB(24, 4, 24, 16), child: Text( - user?.primaryEmail ?? '', - style: Theme.of(context).textTheme.caption, + user.primaryEmail ?? '', + style: Theme.of(context).textTheme.bodySmall, overflow: TextOverflow.fade, ), ) @@ -619,7 +630,7 @@ class DashboardState extends State { // Create the inbox tile with an infinite badge count, since there's lots of space we don't need to limit the count to 99+ _navDrawerInbox() => ListTile( - title: Text(L10n(context).inbox), + title: Text(L10n(context).inbox, style: Theme.of(context).textTheme.titleMedium), onTap: () => _navigateToInbox(context), leading: Padding( padding: const EdgeInsets.only(left: 8.0), @@ -633,7 +644,7 @@ class DashboardState extends State { ); _navDrawerManageStudents() => ListTile( - title: Text(L10n(context).manageStudents), + title: Text(L10n(context).manageStudents, style: Theme.of(context).textTheme.titleMedium), onTap: () => _navigateToManageStudents(context), leading: Padding( padding: const EdgeInsets.only(left: 8.0), @@ -642,7 +653,7 @@ class DashboardState extends State { ); _navDrawerSettings() => ListTile( - title: Text(L10n(context).settings), + title: Text(L10n(context).settings, style: Theme.of(context).textTheme.titleMedium), onTap: () => _navigateToSettings(context), leading: Padding( padding: const EdgeInsets.only(left: 8.0), @@ -651,7 +662,7 @@ class DashboardState extends State { ); _navDrawerHelp() => ListTile( - title: Text(L10n(context).help), + title: Text(L10n(context).help, style: Theme.of(context).textTheme.titleMedium), onTap: () => _navigateToHelp(context), leading: Padding( padding: const EdgeInsets.only(left: 8.0), @@ -660,7 +671,7 @@ class DashboardState extends State { ); _navDrawerLogOut() => ListTile( - title: Text(L10n(context).logOut), + title: Text(L10n(context).logOut, style: Theme.of(context).textTheme.titleMedium), leading: Padding( padding: const EdgeInsets.only(left: 8.0), child: SvgPicture.asset('assets/svg/ic_logout.svg', height: 24, width: 24,), @@ -672,12 +683,12 @@ class DashboardState extends State { return AlertDialog( content: Text(L10n(context).logoutConfirmation), actions: [ - FlatButton( + TextButton( child: Text( MaterialLocalizations.of(context).cancelButtonLabel), onPressed: () => Navigator.of(context).pop(), ), - FlatButton( + TextButton( child: Text(MaterialLocalizations.of(context).okButtonLabel), onPressed: () => _performLogOut(context), @@ -690,7 +701,7 @@ class DashboardState extends State { ); _navDrawerSwitchUsers() => ListTile( - title: Text(L10n(context).switchUsers), + title: Text(L10n(context).switchUsers, style: Theme.of(context).textTheme.titleMedium), leading: Padding( padding: const EdgeInsets.only(left: 8.0), child: SvgPicture.asset('assets/svg/ic_change_user.svg', height: 24, width: 24), @@ -703,7 +714,7 @@ class DashboardState extends State { padding: const EdgeInsets.only(left: 8.0), child: Icon(CanvasIcons.masquerade), ), - title: Text(L10n(context).actAsUser), + title: Text(L10n(context).actAsUser, style: Theme.of(context).textTheme.titleMedium), onTap: () { Navigator.of(context).pop(); locator().push(context, MasqueradeScreen()); @@ -715,10 +726,10 @@ class DashboardState extends State { padding: const EdgeInsets.only(left: 8.0), child: Icon(CanvasIcons.masquerade), ), - title: Text(L10n(context).stopActAsUser), + title: Text(L10n(context).stopActAsUser, style: Theme.of(context).textTheme.titleMedium), onTap: () { Navigator.of(context).pop(); - MasqueradeUI.showMasqueradeCancelDialog(Navigator.of(context).widget.key); + MasqueradeUI.showMasqueradeCancelDialog(GlobalKey(), context); }, ); @@ -732,8 +743,8 @@ class DashboardState extends State { future: PackageInfo.fromPlatform(), builder: (BuildContext context, AsyncSnapshot snapshot) { return Text( - L10n(context).appVersion(snapshot.data?.version), - style: Theme.of(context).textTheme.subtitle2, + L10n(context).appVersion(snapshot.data?.version ?? ''), + style: Theme.of(context).textTheme.titleSmall, ); }, ), diff --git a/apps/flutter_parent/lib/screens/dashboard/inbox_notifier.dart b/apps/flutter_parent/lib/screens/dashboard/inbox_notifier.dart index 817eaa10b3..4d9e30a543 100644 --- a/apps/flutter_parent/lib/screens/dashboard/inbox_notifier.dart +++ b/apps/flutter_parent/lib/screens/dashboard/inbox_notifier.dart @@ -16,13 +16,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_parent/network/api/inbox_api.dart'; import 'package:flutter_parent/utils/service_locator.dart'; -class InboxCountNotifier extends ValueNotifier { +class InboxCountNotifier extends ValueNotifier { InboxCountNotifier() : super(0); update() async { try { var unreadCount = await locator().getUnreadCount(); - value = int.parse(unreadCount?.count?.asString); + value = int.tryParse(unreadCount?.count.asString ?? ''); } catch (e) { print(e); } diff --git a/apps/flutter_parent/lib/screens/dashboard/selected_student_notifier.dart b/apps/flutter_parent/lib/screens/dashboard/selected_student_notifier.dart index 720ce9b6f0..87c7ddf7db 100644 --- a/apps/flutter_parent/lib/screens/dashboard/selected_student_notifier.dart +++ b/apps/flutter_parent/lib/screens/dashboard/selected_student_notifier.dart @@ -17,7 +17,7 @@ import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/screens/dashboard/alert_notifier.dart'; import 'package:flutter_parent/utils/service_locator.dart'; -class SelectedStudentNotifier extends ValueNotifier { +class SelectedStudentNotifier extends ValueNotifier { SelectedStudentNotifier() : super(null); update(User student) { diff --git a/apps/flutter_parent/lib/screens/dashboard/student_expansion_widget.dart b/apps/flutter_parent/lib/screens/dashboard/student_expansion_widget.dart index 6d76d68984..7c9c446242 100644 --- a/apps/flutter_parent/lib/screens/dashboard/student_expansion_widget.dart +++ b/apps/flutter_parent/lib/screens/dashboard/student_expansion_widget.dart @@ -15,7 +15,7 @@ import 'package:flutter/widgets.dart'; class StudentExpansionWidget extends StatefulWidget { - final Widget child; + final Widget? child; final bool expand; StudentExpansionWidget({this.expand = false, this.child}); @@ -25,8 +25,8 @@ class StudentExpansionWidget extends StatefulWidget { } class StudentExpansionWidgetState extends State with SingleTickerProviderStateMixin { - AnimationController expandController; - Animation animation; + late AnimationController expandController; + late Animation animation; @override void initState() { diff --git a/apps/flutter_parent/lib/screens/dashboard/student_horizontal_list_view.dart b/apps/flutter_parent/lib/screens/dashboard/student_horizontal_list_view.dart index 6327c42d57..c28b82014c 100644 --- a/apps/flutter_parent/lib/screens/dashboard/student_horizontal_list_view.dart +++ b/apps/flutter_parent/lib/screens/dashboard/student_horizontal_list_view.dart @@ -25,8 +25,8 @@ import 'package:provider/provider.dart'; class StudentHorizontalListView extends StatefulWidget { final List _students; - final Function onTap; - final Function onAddStudent; + final Function? onTap; + final Function? onAddStudent; StudentHorizontalListView(this._students, {this.onTap, this.onAddStudent}); @@ -53,10 +53,10 @@ class StudentHorizontalListViewState extends State { behavior: HitTestBehavior.translucent, onTap: () { ApiPrefs.updateCurrentLogin((b) => b..selectedStudentId = student.id); - ParentTheme.of(context).setSelectedStudent(student.id); + ParentTheme.of(context)?.setSelectedStudent(student.id); Provider.of(context, listen: false).update(student); ApiPrefs.setCurrentStudent(student); - widget.onTap(); + if (widget.onTap != null) widget.onTap!(); }, child: Semantics( label: L10n(context).tapToSelectStudent, @@ -76,9 +76,9 @@ class StudentHorizontalListViewState extends State { ), SizedBox(height: 8), Text( - student.shortName, + student.shortName ?? '', key: Key("${student.shortName}_text"), - style: Theme.of(context).textTheme.subtitle2.copyWith(color: ParentTheme.of(context).onSurfaceColor), + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: ParentTheme.of(context)?.onSurfaceColor), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), @@ -95,37 +95,43 @@ class StudentHorizontalListViewState extends State { child: Container( width: 120, height: 92, - child: Column( + child: ListView( children: [ SizedBox(height: 12), Container( width: 48, height: 48, - child: RaisedButton( - padding: EdgeInsets.zero, - color: Theme.of(context).scaffoldBackgroundColor, + child: ElevatedButton( child: Semantics( label: L10n(context).tapToPairNewStudent, child: Icon( Icons.add, - color: Theme.of(context).accentColor, + color: Theme.of(context).colorScheme.secondary, ), ), - shape: CircleBorder( - side: BorderSide( - color: ParentTheme.of(context).isDarkMode ? Theme.of(context).accentColor : Colors.white, - width: 1), + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + surfaceTintColor: Theme.of(context).canvasColor, + shape: CircleBorder( + side: BorderSide( + color: ParentTheme.of(context)?.isDarkMode == true ? Theme.of(context).colorScheme.secondary : Colors.white, + width: 1), + ), + elevation: 8, ), - elevation: 8, onPressed: () { - locator().pairNewStudent(context, () => widget.onAddStudent()); + locator().pairNewStudent(context, () => { if (widget.onAddStudent != null) widget.onAddStudent!() }); }, ), ), SizedBox(height: 8), - Text( - L10n(context).addStudent, - style: Theme.of(context).textTheme.subtitle2.copyWith(color: ParentTheme.of(context).onSurfaceColor), + Align( + alignment: Alignment.center, + child: Text( + L10n(context).addStudent, + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: ParentTheme.of(context)?.onSurfaceColor), + ), ), ], ), diff --git a/apps/flutter_parent/lib/screens/domain_search/domain_search_interactor.dart b/apps/flutter_parent/lib/screens/domain_search/domain_search_interactor.dart index 36c49aa52b..6f419c7134 100644 --- a/apps/flutter_parent/lib/screens/domain_search/domain_search_interactor.dart +++ b/apps/flutter_parent/lib/screens/domain_search/domain_search_interactor.dart @@ -18,7 +18,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; class DomainSearchInteractor { - Future> performSearch(String query) { + Future?> performSearch(String query) { return locator().searchDomains(query); } diff --git a/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart b/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart index 06a752f13a..6c8f8b31a5 100644 --- a/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart +++ b/apps/flutter_parent/lib/screens/domain_search/domain_search_screen.dart @@ -15,7 +15,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; -import 'package:flutter_parent/models/school_domain.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/web_login/web_login_screen.dart'; @@ -31,9 +30,9 @@ class DomainSearchScreen extends StatefulWidget { @visibleForTesting static final GlobalKey helpDialogBodyKey = GlobalKey(); - const DomainSearchScreen({Key key, this.loginFlow}) : super(key: key); + const DomainSearchScreen({this.loginFlow, super.key}); - final LoginFlow loginFlow; + final LoginFlow? loginFlow; @override _DomainSearchScreenState createState() => _DomainSearchScreenState(); @@ -42,7 +41,7 @@ class DomainSearchScreen extends StatefulWidget { class _DomainSearchScreenState extends State { var _interactor = locator(); - var _schoolDomains = List(); + var _schoolDomains = []; /// The minimum length of a trimmed query required to trigger a search static const int _MIN_SEARCH_LENGTH = 2; @@ -57,7 +56,7 @@ class _DomainSearchScreenState extends State { bool _error = false; /// The current query, tracked to help prevent race conditions when a previous search completes after a more recent search - String _currentQuery; + String? _currentQuery; Debouncer _debouncer = Debouncer(Duration(milliseconds: 500)); @@ -86,7 +85,7 @@ class _DomainSearchScreenState extends State { setState(() { _loading = false; _error = false; - _schoolDomains = domains; + if (domains != null) _schoolDomains = domains; }); }).catchError((error) { if (_currentQuery != query) return; @@ -108,13 +107,13 @@ class _DomainSearchScreenState extends State { L10n(context).findSchool, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), actions: [ MaterialButton( minWidth: 20, highlightColor: Colors.transparent, - splashColor: Theme.of(context).accentColor.withAlpha(100), - textColor: Theme.of(context).accentColor, + splashColor: Theme.of(context).colorScheme.secondary.withAlpha(100), + textColor: Theme.of(context).colorScheme.secondary, shape: CircleBorder(side: BorderSide(color: Colors.transparent)), onPressed: _query.isEmpty ? null : () => _next(context), child: Text( @@ -195,7 +194,7 @@ class _DomainSearchScreenState extends State { var item = _schoolDomains[index]; return ListTile( title: Text(item.name), - onTap: () { + onTap: () async { final accountName = (item.name == null || item.name.isEmpty) ? item.domain : item.name; return locator().pushRoute(context, PandaRouter.loginWeb(item.domain, accountName: accountName, authenticationProvider: item.authenticationProvider)); @@ -206,10 +205,12 @@ class _DomainSearchScreenState extends State { ), if (_schoolDomains.isNotEmpty) Divider(height: 0), Center( - child: FlatButton( + child: TextButton( key: Key('help-button'), child: Text(L10n(context).domainSearchHelpLabel), - textTheme: ButtonTextTheme.accent, + style: TextButton.styleFrom( + textStyle: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), onPressed: () { _showHelpDialog(context); }, @@ -238,12 +239,12 @@ class _DomainSearchScreenState extends State { inputSpans: [ TextSpan( text: canvasGuidesText, - style: TextStyle(color: Theme.of(context).accentColor), + style: TextStyle(color: Theme.of(context).colorScheme.secondary), recognizer: TapGestureRecognizer()..onTap = _interactor.openCanvasGuides, ), TextSpan( text: canvasSupportText, - style: TextStyle(color: Theme.of(context).accentColor), + style: TextStyle(color: Theme.of(context).colorScheme.secondary), recognizer: TapGestureRecognizer()..onTap = _interactor.openCanvasSupport, ), ], @@ -251,7 +252,7 @@ class _DomainSearchScreenState extends State { key: DomainSearchScreen.helpDialogBodyKey, ), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).ok), onPressed: () => Navigator.of(context).pop(), ), @@ -260,8 +261,8 @@ class _DomainSearchScreenState extends State { }); } - TextSpan _helpBodySpan({@required String text, @required List inputSpans}) { - var indexedSpans = inputSpans.map((it) => MapEntry(text.indexOf(it.text), it)).toList(); + TextSpan _helpBodySpan({required String text, required List inputSpans}) { + var indexedSpans = inputSpans.map((it) => MapEntry(text.indexOf(it.text!), it)).toList(); indexedSpans.sort((a, b) => a.key.compareTo(b.key)); int index = 0; @@ -270,7 +271,7 @@ class _DomainSearchScreenState extends State { for (var indexedSpan in indexedSpans) { spans.add(TextSpan(text: text.substring(index, indexedSpan.key))); spans.add(indexedSpan.value); - index = indexedSpan.key + indexedSpan.value.text.length; + index = indexedSpan.key + indexedSpan.value.text!.length; } spans.add(TextSpan(text: text.substring(index))); @@ -278,11 +279,14 @@ class _DomainSearchScreenState extends State { } void _next(BuildContext context) { - var domain = _query; + String domain = _query; RegExp regExp = new RegExp(r'(.*)([a-zA-Z0-9]){1}'); - if (regExp.hasMatch(domain)) domain = regExp.stringMatch(domain); + if (regExp.hasMatch(domain)) domain = regExp.stringMatch(domain)!; if (domain.startsWith('www.')) domain = domain.substring(4); // Strip off www. if they typed it if (!domain.contains('.') || domain.endsWith('.beta')) domain += '.instructure.com'; - locator().pushRoute(context, PandaRouter.loginWeb(domain, accountName: domain, loginFlow: widget.loginFlow)); + if (widget.loginFlow != null) + locator().pushRoute(context, PandaRouter.loginWeb(domain, accountName: domain, loginFlow: widget.loginFlow!)); + else + locator().pushRoute(context, PandaRouter.loginWeb(domain, accountName: domain)); } } diff --git a/apps/flutter_parent/lib/screens/events/event_details_interactor.dart b/apps/flutter_parent/lib/screens/events/event_details_interactor.dart index 44203f3f77..18ecd22620 100644 --- a/apps/flutter_parent/lib/screens/events/event_details_interactor.dart +++ b/apps/flutter_parent/lib/screens/events/event_details_interactor.dart @@ -21,14 +21,14 @@ import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class EventDetailsInteractor { - Future loadEvent(String eventId, bool forceRefresh) { + Future loadEvent(String? eventId, bool forceRefresh) { return locator().getEvent(eventId, forceRefresh); } - Future loadReminder(String eventId) async { + Future loadReminder(String? eventId) async { final reminder = await locator().getByItem( ApiPrefs.getDomain(), - ApiPrefs.getUser().id, + ApiPrefs.getUser()?.id, Reminder.TYPE_EVENT, eventId, ); @@ -37,7 +37,7 @@ class EventDetailsInteractor { to remove it from the database. Given that we cannot time travel (yet), if the reminder we just retrieved has a date set in the past then we will opt to delete it here. */ if (reminder?.date?.isBefore(DateTime.now()) == true) { - await deleteReminder(reminder); + await deleteReminder(reminder!); return null; } @@ -47,14 +47,14 @@ class EventDetailsInteractor { Future createReminder( AppLocalizations l10n, DateTime date, - String eventId, - String courseId, - String title, + String? eventId, + String? courseId, + String? title, String body, ) async { var reminder = Reminder((b) => b ..userDomain = ApiPrefs.getDomain() - ..userId = ApiPrefs.getUser().id + ..userId = ApiPrefs.getUser()?.id ..type = Reminder.TYPE_EVENT ..itemId = eventId ..courseId = courseId @@ -62,14 +62,16 @@ class EventDetailsInteractor { ); // Saving to the database will generate an ID for this reminder - reminder = await locator().insert(reminder); - - await locator().scheduleReminder(l10n, title, body, reminder); + var insertedReminder = await locator().insert(reminder); + if (insertedReminder != null) { + reminder = insertedReminder; + await locator().scheduleReminder(l10n, title, body, reminder); + } } - Future deleteReminder(Reminder reminder) async { + Future deleteReminder(Reminder? reminder) async { if (reminder == null) return; - await locator().deleteNotification(reminder.id); - await locator().deleteById(reminder.id); + await locator().deleteNotification(reminder.id!); + await locator().deleteById(reminder.id!); } } diff --git a/apps/flutter_parent/lib/screens/events/event_details_screen.dart b/apps/flutter_parent/lib/screens/events/event_details_screen.dart index dd260c7775..7b2eb4700b 100644 --- a/apps/flutter_parent/lib/screens/events/event_details_screen.dart +++ b/apps/flutter_parent/lib/screens/events/event_details_screen.dart @@ -11,6 +11,7 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/reminder.dart'; @@ -30,27 +31,26 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:intl/intl.dart'; class EventDetailsScreen extends StatefulWidget { - final ScheduleItem event; - final String eventId; + final ScheduleItem? event; + final String? eventId; // Course ID is used for messaging. The message FAB will not be shown if it or current student is null. - final String courseId; + final String? courseId; EventDetailsScreen.withEvent({ - Key key, - this.event, + required this.event, this.courseId, - }) : assert(event != null), - eventId = event.id, - super(key: key); + super.key + }) : assert(event != null), + eventId = event!.id; EventDetailsScreen.withId({ - Key key, - this.eventId, + required this.eventId, this.courseId, - }) : assert(eventId != null), - event = null, - super(key: key); + super.key + }) : + assert(eventId != null), + event = null; @override _EventDetailsScreenState createState() => _EventDetailsScreenState(); @@ -59,9 +59,9 @@ class EventDetailsScreen extends StatefulWidget { class _EventDetailsScreenState extends State { GlobalKey _refreshKey = GlobalKey(); - Future _eventFuture; + late Future _eventFuture; - Future _loadEvent({bool forceRefresh = false}) => _interactor.loadEvent(widget.eventId, forceRefresh); + Future _loadEvent({bool forceRefresh = false}) => _interactor.loadEvent(widget.eventId, forceRefresh); EventDetailsInteractor get _interactor => locator(); @@ -79,7 +79,7 @@ class _EventDetailsScreenState extends State { Widget build(BuildContext context) { return FutureBuilder( future: _eventFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { return Scaffold( appBar: AppBar(title: Text(L10n(context).eventDetailsTitle)), floatingActionButton: _fab(snapshot), @@ -98,11 +98,11 @@ class _EventDetailsScreenState extends State { ); } - Widget _body(BuildContext context, AsyncSnapshot snapshot) { + Widget _body(BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return ErrorPandaWidget( L10n(context).unexpectedError, - () => _refreshKey.currentState.show(), + () => _refreshKey.currentState?.show(), ); } else if (!snapshot.hasData) { return LoadingIndicator(); @@ -111,9 +111,9 @@ class _EventDetailsScreenState extends State { } } - Widget _fab(AsyncSnapshot snapshot) { - User student = ApiPrefs.getCurrentStudent(); - if (!snapshot.hasData || widget.courseId == null || student.id == null || student.name == null) { + Widget? _fab(AsyncSnapshot snapshot) { + User? student = ApiPrefs.getCurrentStudent(); + if (!snapshot.hasData || widget.courseId == null || student?.id == null || student?.name == null) { // The data hasn't loaded, or course/student info is missing (e.g. if we deep linked to this page) return null; } @@ -122,11 +122,11 @@ class _EventDetailsScreenState extends State { tooltip: L10n(context).assignmentMessageHint, child: Padding(padding: const EdgeInsets.only(left: 4, top: 4), child: Icon(CanvasIconsSolid.comment)), onPressed: () { - final event = snapshot.data; - String subject = L10n(context).eventSubjectMessage(student.name, event.title); - String postscript = L10n(context).messageLinkPostscript(student.name, event.htmlUrl); + final event = snapshot.data!; + String subject = L10n(context).eventSubjectMessage(student!.name, event.title!); + String postscript = L10n(context).messageLinkPostscript(student.name, event.htmlUrl ?? ''); Widget screen = CreateConversationScreen( - widget.courseId, + widget.courseId!, student.id, subject, postscript, @@ -138,40 +138,39 @@ class _EventDetailsScreenState extends State { } class _EventDetails extends StatelessWidget { - final ScheduleItem event; - final String courseId; + final ScheduleItem? event; + final String? courseId; - const _EventDetails(this.event, this.courseId, {Key key}) - : assert(event != null), - super(key: key); + const _EventDetails(this.event, this.courseId, {super.key}); @override Widget build(BuildContext context) { final l10n = L10n(context); // Get the date strings - String dateLine1, dateLine2; - final date = event.startAt ?? event.endAt; - if (event.isAllDay) { + String? dateLine1, dateLine2; + final date = event?.startAt ?? event?.endAt; + if (event?.isAllDay == true) { dateLine1 = _dateFormat(date); - } else if (event.startAt != null && event.endAt != null && event.startAt != event.endAt) { + dateLine2 = ''; + } else if (event?.startAt != null && event?.endAt != null && event?.startAt != event?.endAt) { dateLine1 = _dateFormat(date); - dateLine2 = l10n.eventTime(_timeFormat(event.startAt), _timeFormat(event.endAt)); + dateLine2 = l10n.eventTime(_timeFormat(event?.startAt!)!, _timeFormat(event?.endAt!)!); } else { dateLine1 = _dateFormat(date); dateLine2 = _timeFormat(date); } // Get the location strings - String locationLine1, locationLine2; - if ((event.locationAddress == null || event.locationAddress.isEmpty) && - (event.locationName == null || event.locationName.isEmpty)) { + String? locationLine1, locationLine2; + if ((event?.locationAddress == null || event?.locationAddress?.isEmpty == true) && + (event?.locationName == null || event?.locationName?.isEmpty == true)) { locationLine1 = l10n.eventNoLocation; - } else if (event.locationName == null || event.locationName.isEmpty) { - locationLine1 = event.locationAddress; + } else if (event?.locationName == null || event?.locationName?.isEmpty == true) { + locationLine1 = event?.locationAddress ?? ''; } else { - locationLine1 = event.locationName; - locationLine2 = event.locationAddress; + locationLine1 = event?.locationName ?? ''; + locationLine2 = event?.locationAddress ?? ''; } final textTheme = Theme.of(context).textTheme; @@ -180,7 +179,7 @@ class _EventDetails extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0), children: [ SizedBox(height: 16), - Text(event.title ?? '', style: textTheme.headline4, key: ValueKey('event_details_title')), + Text(event?.title ?? '', style: textTheme.headlineMedium, key: ValueKey('event_details_title')), SizedBox(height: 16), Divider(), _SimpleTile(label: l10n.eventDateLabel, line1: dateLine1, line2: dateLine2, keyPrefix: 'event_details_date'), @@ -192,42 +191,42 @@ class _EventDetails extends StatelessWidget { keyPrefix: 'event_details_location'), Divider(), _SimpleHeader(label: l10n.assignmentRemindMeLabel), - _RemindMe(event, courseId, [dateLine1, dateLine2].where((it) => it != null).join('\n')), + _RemindMe(event, courseId, [dateLine1, dateLine2].nonNulls.where((d) => d.isNotEmpty).join('\n')), Divider(), - HtmlDescriptionTile(html: event.description), + HtmlDescriptionTile(html: event?.description), // Don't show the bottom divider if there's no content (no empty message shown either) - if (event.description != null && event.description.isNotEmpty) + if (event?.description != null && event?.description?.isNotEmpty == true) Divider(), ], ); } - String _dateFormat(DateTime time) { + String? _dateFormat(DateTime? time) { return time == null ? null : DateFormat.EEEE(supportedDateLocale).add_yMMMd().format(time.toLocal()); } - String _timeFormat(DateTime time) { + String? _timeFormat(DateTime? time) { return time == null ? null : DateFormat.jm(supportedDateLocale).format(time.toLocal()); } } class _RemindMe extends StatefulWidget { - final ScheduleItem event; - final String courseId; + final ScheduleItem? event; + final String? courseId; final String formattedDate; - const _RemindMe(this.event, this.courseId, this.formattedDate, {Key key}) : super(key: key); + const _RemindMe(this.event, this.courseId, this.formattedDate, {super.key}); @override _RemindMeState createState() => _RemindMeState(); } class _RemindMeState extends State<_RemindMe> { - Future _reminderFuture; + late Future _reminderFuture; EventDetailsInteractor _interactor = locator(); - Future _loadReminder() => _interactor.loadReminder(widget.event.id); + Future _loadReminder() => _interactor.loadReminder(widget.event?.id); @override void initState() { @@ -240,25 +239,25 @@ class _RemindMeState extends State<_RemindMe> { TextTheme textTheme = Theme.of(context).textTheme; return FutureBuilder( future: _reminderFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - Reminder reminder = snapshot.data; + builder: (BuildContext context, AsyncSnapshot snapshot) { + Reminder? reminder = snapshot.data; return SwitchListTile( contentPadding: EdgeInsets.zero, value: reminder != null, title: Text( reminder?.date == null ? L10n(context).eventRemindMeDescription : L10n(context).eventRemindMeSet, - style: textTheme.subtitle1, + style: textTheme.titleMedium, ), subtitle: reminder == null ? null : Padding( padding: const EdgeInsets.only(top: 8), child: Text( - reminder.date.l10nFormat(L10n(context).dateAtTime), - style: textTheme.subtitle1.copyWith(color: ParentTheme.of(context).studentColor), + reminder.date.l10nFormat(L10n(context).dateAtTime)!, + style: textTheme.titleMedium?.copyWith(color: ParentTheme.of(context)?.studentColor), ), ), - onChanged: (checked) => _handleAlarmSwitch(context, widget.event, checked, reminder, widget.formattedDate), + onChanged: (checked) => _handleAlarmSwitch(context, widget.event, checked, reminder, widget.formattedDate) ); }, ); @@ -266,19 +265,19 @@ class _RemindMeState extends State<_RemindMe> { _handleAlarmSwitch( BuildContext context, - ScheduleItem event, + ScheduleItem? event, bool checked, - Reminder reminder, + Reminder? reminder, String formattedDate, ) async { if (reminder != null) _interactor.deleteReminder(reminder); if (checked) { var now = DateTime.now(); - var eventDate = event.isAllDay ? event.allDayDate.toLocal() : event.startAt.toLocal(); - var initialDate = eventDate?.isAfter(now) == true ? eventDate : now; + var eventDate = event?.isAllDay == true ? event?.allDayDate?.toLocal() : event?.startAt?.toLocal(); + var initialDate = eventDate?.isAfter(now) == true ? eventDate! : now; - DateTime date; - TimeOfDay time; + DateTime? date; + TimeOfDay? time; date = await showDatePicker( context: context, @@ -295,9 +294,9 @@ class _RemindMeState extends State<_RemindMe> { await _interactor.createReminder( L10n(context), reminderDate, - event.id, + event?.id, widget.courseId, - event.title, + event?.title, formattedDate, ); } @@ -311,9 +310,10 @@ class _RemindMeState extends State<_RemindMe> { } class _SimpleTile extends StatelessWidget { - final String label, line1, line2, keyPrefix; + final String label, keyPrefix; + final String? line1, line2; - const _SimpleTile({Key key, this.label, this.line1, this.line2, this.keyPrefix}) : super(key: key); + const _SimpleTile({required this.label, this.line1, this.line2, required this.keyPrefix, super.key}); @override Widget build(BuildContext context) { @@ -323,9 +323,9 @@ class _SimpleTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _SimpleHeader(label: label), - Text(line1 ?? '', style: textTheme.subtitle1, key: ValueKey('${keyPrefix}_line1')), + Text(line1 ?? '', style: textTheme.titleMedium, key: ValueKey('${keyPrefix}_line1')), if (line2 != null) SizedBox(height: 8), - if (line2 != null) Text(line2, style: textTheme.subtitle1, key: ValueKey('${keyPrefix}_line2')), + if (line2 != null) Text(line2!, style: textTheme.titleMedium, key: ValueKey('${keyPrefix}_line2')), SizedBox(height: 16), ], ); @@ -335,7 +335,7 @@ class _SimpleTile extends StatelessWidget { class _SimpleHeader extends StatelessWidget { final String label; - const _SimpleHeader({Key key, this.label}) : super(key: key); + const _SimpleHeader({required this.label, super.key}); @override Widget build(BuildContext context) { @@ -343,7 +343,7 @@ class _SimpleHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16), - Text(label, style: Theme.of(context).textTheme.overline), + Text(label, style: Theme.of(context).textTheme.labelSmall), SizedBox(height: 8), ], ); diff --git a/apps/flutter_parent/lib/screens/help/help_screen.dart b/apps/flutter_parent/lib/screens/help/help_screen.dart index 3145a659f3..4e468a5720 100644 --- a/apps/flutter_parent/lib/screens/help/help_screen.dart +++ b/apps/flutter_parent/lib/screens/help/help_screen.dart @@ -15,15 +15,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/help_link.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; -import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/utils/common_widgets/error_report/error_report_dialog.dart'; import 'package:flutter_parent/utils/common_widgets/loading_indicator.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; -import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; import 'package:flutter_parent/utils/veneers/android_intent_veneer.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'help_screen_interactor.dart'; @@ -34,8 +32,8 @@ class HelpScreen extends StatefulWidget { class _HelpScreenState extends State { final _interactor = locator(); - Future> _helpLinksFuture; - AppLocalizations l10n; + late Future> _helpLinksFuture; + late AppLocalizations l10n; @override void initState() { @@ -55,35 +53,30 @@ class _HelpScreenState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(l10n.help), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: _body), ); }); } - Widget _success(List links) => ListView(children: _generateLinks(links)); + Widget _success(List? links) => ListView(children: _generateLinks(links)); - List _generateLinks(List links) { - List helpLinks = List.from(links.map( + List _generateLinks(List? links) { + List helpLinks = List.from(links?.map( (l) => ListTile( - title: Text(l.text), - subtitle: Text(l.subtext), + title: Text(l.text, style: Theme.of(context).textTheme.titleMedium), + subtitle: Text(l.subtext, style: Theme.of(context).textTheme.bodySmall), onTap: () => _linkClick(l), ), - )); + ) ?? []); // Add in the legal and share the love tiles helpLinks.addAll([ ListTile( - title: Text(l10n.helpShareLoveLabel), - subtitle: Text(l10n.helpShareLoveDescription), + title: Text(l10n.helpShareLoveLabel, style: Theme.of(context).textTheme.titleMedium), + subtitle: Text(l10n.helpShareLoveDescription, style: Theme.of(context).textTheme.bodySmall), onTap: _showShareLove, - ), - ListTile( - title: Text(l10n.helpLegalLabel), - subtitle: Text(l10n.helpLegalDescription), - onTap: () => _showLegal(), ) ]); @@ -132,7 +125,7 @@ class _HelpScreenState extends State { final parentId = ApiPrefs.getUser()?.id ?? 0; final email = ApiPrefs.getUser()?.primaryEmail ?? ''; final domain = ApiPrefs.getDomain() ?? ''; - final locale = ApiPrefs.effectiveLocale().toLanguageTag(); + final locale = ApiPrefs.effectiveLocale()?.toLanguageTag(); PackageInfo package = await PackageInfo.fromPlatform(); @@ -152,6 +145,4 @@ class _HelpScreenState extends State { } void _showShareLove() => locator().launchAppStore(); - - void _showLegal() => locator().pushRoute(context, PandaRouter.legal()); } diff --git a/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart b/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart index 18a32fe18b..cc143cd67d 100644 --- a/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart +++ b/apps/flutter_parent/lib/screens/help/help_screen_interactor.dart @@ -20,7 +20,9 @@ import 'package:flutter_parent/utils/service_locator.dart'; class HelpScreenInteractor { Future> getObserverCustomHelpLinks({bool forceRefresh = false}) async { - HelpLinks links = await locator.get().getHelpLinks(forceRefresh: forceRefresh); + HelpLinks? links = await locator.get().getHelpLinks(forceRefresh: forceRefresh); + + if (links == null) return Future.value([]); // Filter observer custom links if we have any, otherwise return an empty list return Future.value(filterObserverLinks( diff --git a/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart b/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart index c64896bc5d..531152fe15 100644 --- a/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart +++ b/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart @@ -12,7 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/terms_of_service.dart'; @@ -23,19 +22,20 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/web_view_utils.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import '../../utils/veneers/android_intent_veneer.dart'; class TermsOfUseScreen extends StatefulWidget { - final String accountId; - final String domain; + final String? accountId; + final String? domain; - const TermsOfUseScreen({this.accountId, this.domain, Key key}) : super(key: key); + const TermsOfUseScreen({this.accountId, this.domain, super.key}); @override _TermsOfUseScreenState createState() => _TermsOfUseScreenState(); } class _TermsOfUseScreenState extends State { - Future _tosFuture; + late Future _tosFuture; @override void initState() { @@ -43,9 +43,9 @@ class _TermsOfUseScreenState extends State { super.initState(); } - Future getTosFuture() { + Future getTosFuture() { return (widget.accountId != null && widget.domain != null) - ? locator().getTermsOfServiceForAccount(widget.accountId, widget.domain) + ? locator().getTermsOfServiceForAccount(widget.accountId!, widget.domain!) : locator().getTermsOfService(); } @@ -55,11 +55,11 @@ class _TermsOfUseScreenState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(L10n(context).termsOfUse), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: FutureBuilder( future: _tosFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { // Loading if (snapshot.connectionState != ConnectionState.done) return LoadingIndicator(); @@ -75,14 +75,23 @@ class _TermsOfUseScreenState extends State { // Content return WebView( - darkMode: ParentTheme.of(context).isWebViewDarkMode, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode, onWebViewCreated: (controller) { - controller.loadHtml(snapshot.data.content, horizontalPadding: 16); + controller.loadHtml(snapshot.data!.content!, horizontalPadding: 16); }, + navigationDelegate: _handleNavigation ); }, ), ), ); } + + NavigationDecision _handleNavigation(NavigationRequest request) { + if (request.url.contains("mailto:")) { + locator().launchEmail(request.url); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + } } diff --git a/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_handler.dart b/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_handler.dart index 85743c3aa5..383a4031b4 100644 --- a/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_handler.dart +++ b/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_handler.dart @@ -26,17 +26,17 @@ enum AttachmentUploadStage { CREATED, UPLOADING, FAILED, FINISHED } class AttachmentHandler with ChangeNotifier { AttachmentHandler(this._file); - final File _file; - Function(AttachmentUploadStage) onStageChange; - double progress = null; - Attachment attachment; + final File? _file; + Function(AttachmentUploadStage)? onStageChange; + double? progress = null; + Attachment? attachment; AttachmentUploadStage _stage = AttachmentUploadStage.CREATED; AttachmentUploadStage get stage => _stage; set stage(AttachmentUploadStage stage) { _stage = stage; - if (onStageChange != null) onStageChange(_stage); + if (onStageChange != null) onStageChange!(_stage); } String get displayName => attachment?.displayName ?? attachment?.filename ?? basename(_file?.path ?? ''); @@ -51,7 +51,7 @@ class AttachmentHandler with ChangeNotifier { try { // Upload the file and monitor progress - attachment = await locator().uploadConversationFile(_file, (current, total) { + attachment = await locator().uploadConversationFile(_file!, (current, total) { progress = total == -1 ? null : current.toDouble() / total; notifyListeners(); }); @@ -85,11 +85,14 @@ class AttachmentHandler with ChangeNotifier { pathProvider.getExternalStorageDirectory(), ]); - var dirPaths = dirs.map((it) => it.absolute.path); - var filePath = _file.absolute.path; + var dirPaths = dirs.map((it) => it?.absolute.path); + var filePath = _file?.absolute.path; - if (dirPaths.any((it) => filePath.startsWith(it))) { - await _file.delete(); + if (dirPaths.any((it) { + if (it == null) return false; + return (filePath?.startsWith(it) == true); + })) { + await _file?.delete(); } } on Error catch (e) { print('Unable to clean up attachment source file'); @@ -101,8 +104,8 @@ class AttachmentHandler with ChangeNotifier { Future deleteAttachment() async { if (attachment == null) return; try { - await locator().deleteFile(attachment.id); - print('Deleted attachment "${attachment.displayName}"'); + await locator().deleteFile(attachment!.id); + print('Deleted attachment "${attachment!.displayName}"'); } on Error catch (e) { print('Unable to delete attachment'); print(e.stackTrace); diff --git a/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker.dart b/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker.dart index fdfaf01c3b..f6c0deb0f4 100644 --- a/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker.dart +++ b/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker.dart @@ -28,7 +28,7 @@ class AttachmentPicker extends StatefulWidget { @override _AttachmentPickerState createState() => _AttachmentPickerState(); - static Future asBottomSheet(BuildContext context) { + static Future asBottomSheet(BuildContext context) { return showModalBottomSheet(context: context, builder: (context) => AttachmentPicker()); } } @@ -43,7 +43,7 @@ class _AttachmentPickerState extends State { Widget _pickerWidget(BuildContext context) { final interactor = locator(); - final iconColor = ParentTheme.of(context).onSurfaceColor; + final iconColor = ParentTheme.of(context)?.onSurfaceColor; return ListView( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, @@ -67,7 +67,7 @@ class _AttachmentPickerState extends State { ); } - Widget _item({Widget icon, String title, GestureTapCallback onTap}) { + Widget _item({required Widget icon, required String title, required GestureTapCallback onTap}) { return ListTile( leading: Container(width: 20, alignment: Alignment.center, child: icon), title: Text(title), @@ -78,6 +78,7 @@ class _AttachmentPickerState extends State { Widget _importingWidget(BuildContext context) { return Container( height: 184, // Match list height: 16 (padding top + bottom) + 56 (item height) * 3 items + width: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -95,7 +96,7 @@ class _AttachmentPickerState extends State { ); } - _performImport(Future Function() import) async { + _performImport(Future Function() import) async { setState(() => _importing = true); var file = await import(); if (file != null) { diff --git a/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker_interactor.dart b/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker_interactor.dart index e6e45df12c..fb4fa0a760 100644 --- a/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker_interactor.dart +++ b/apps/flutter_parent/lib/screens/inbox/attachment_utils/attachment_picker_interactor.dart @@ -21,25 +21,21 @@ import 'package:image_picker/image_picker.dart'; class AttachmentPickerInteractor { final ImagePicker _imagePicker = ImagePicker(); - Future getImageFromCamera() { + Future getImageFromCamera() { return _imagePicker .pickImage(source: ImageSource.camera) - .then((value) => File(value.path)); + .then((value) => File(value!.path)); } - Future getFileFromDevice() { + Future getFileFromDevice() { final result = FilePicker.platform.pickFiles(); - if (result != null) { - return result.then((value) => File(value.files.single.path)); - } else { - return Future.error(""); - } + return result.then((value) => File(value!.files.single.path!)).onError((error, stackTrace) => Future.error("")); } - Future getImageFromGallery() { + Future getImageFromGallery() { return _imagePicker .pickImage(source: ImageSource.gallery) - .then((value) => File(value.path)); + .then((value) => File(value!.path)); } } diff --git a/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_interactor.dart b/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_interactor.dart index 7ec57900de..b40d383b50 100644 --- a/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_interactor.dart +++ b/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_interactor.dart @@ -25,8 +25,8 @@ import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class ConversationDetailsInteractor { - Future getConversation(String id) async { - Conversation conversation = await locator().getConversation(id, refresh: true); + Future getConversation(String id) async { + Conversation? conversation = await locator().getConversation(id, refresh: true); // Fetching a conversation automatically marks it as read, so we'll want to update the inbox count badge locator().update(); @@ -34,13 +34,14 @@ class ConversationDetailsInteractor { return conversation; } - Future addReply(BuildContext context, Conversation conversation, Message message, bool replyAll) async { - return locator().push(context, ConversationReplyScreen(conversation, message, replyAll)); + Future addReply(BuildContext context, Conversation? conversation, Message? message, bool replyAll) async { + Conversation? r = await locator().push(context, ConversationReplyScreen(conversation, message, replyAll)); + return r; } - String getCurrentUserId() => ApiPrefs.getUser().id; + String? getCurrentUserId() => ApiPrefs.getUser()?.id; - void viewAttachment(BuildContext context, Attachment attachment) { + Future viewAttachment(BuildContext context, Attachment attachment) async { locator().push(context, ViewAttachmentScreen(attachment)); } } diff --git a/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_screen.dart b/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_screen.dart index dff31b0c68..beeca50256 100644 --- a/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_screen.dart +++ b/apps/flutter_parent/lib/screens/inbox/conversation_details/conversation_details_screen.dart @@ -30,15 +30,15 @@ import 'conversation_details_interactor.dart'; class ConversationDetailsScreen extends StatefulWidget { final String conversationId; - final String conversationSubject; - final String courseName; + final String? conversationSubject; + final String? courseName; const ConversationDetailsScreen({ - Key key, - this.conversationId, + required this.conversationId, this.conversationSubject, this.courseName, - }) : super(key: key); + super.key + }); @override _ConversationDetailsScreenState createState() => _ConversationDetailsScreenState(); @@ -46,7 +46,7 @@ class ConversationDetailsScreen extends StatefulWidget { class _ConversationDetailsScreenState extends State { ConversationDetailsInteractor _interactor = locator(); - Future _conversationFuture; + late Future _conversationFuture; bool _hasBeenUpdated = false; @@ -67,7 +67,7 @@ class _ConversationDetailsScreenState extends State { child: DefaultParentTheme( builder: (context) => FutureBuilder( future: _conversationFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) => Scaffold( + builder: (BuildContext context, AsyncSnapshot snapshot) => Scaffold( appBar: _appBar(context), body: _body(context, snapshot), floatingActionButton: _fab(context, snapshot), @@ -77,28 +77,28 @@ class _ConversationDetailsScreenState extends State { ); } - Widget _appBar(BuildContext context) { + AppBar _appBar(BuildContext context) { return AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - widget.conversationSubject == null || widget.conversationSubject.isEmpty + widget.conversationSubject == null || widget.conversationSubject!.isEmpty ? L10n(context).noSubject - : widget.conversationSubject, + : widget.conversationSubject!, key: ValueKey('subjectText')), - if (widget.courseName != null && widget.courseName.isNotEmpty) - Text(widget.courseName, style: Theme.of(context).textTheme.caption, key: ValueKey('courseText')), + if (widget.courseName != null && widget.courseName!.isNotEmpty) + Text(widget.courseName!, style: Theme.of(context).textTheme.bodySmall, key: ValueKey('courseText')), ], ), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ); } - Widget _fab(BuildContext context, AsyncSnapshot snapshot) { + Widget _fab(BuildContext context, AsyncSnapshot snapshot) { if (!snapshot.hasData) return Container(); - Conversation conversation = snapshot.data; + Conversation conversation = snapshot.data!; return FloatingActionButton( child: Icon(CanvasIconsSolid.reply, size: 20), tooltip: L10n(context).reply, @@ -112,14 +112,14 @@ class _ConversationDetailsScreenState extends State { children: [ ListTile( leading: Icon(CanvasIconsSolid.reply, size: 20, color: Theme.of(context).iconTheme.color), - title: Text(L10n(context).reply), + title: Text(L10n(context).reply, style: Theme.of(context).textTheme.bodyMedium), onTap: () { Navigator.of(context).pop(); _reply(context, conversation, null, false); }), ListTile( leading: Icon(CanvasIconsSolid.reply_all_2, size: 20, color: Theme.of(context).iconTheme.color), - title: Text(L10n(context).replyAll), + title: Text(L10n(context).replyAll, style: Theme.of(context).textTheme.bodyMedium), onTap: () { Navigator.of(context).pop(); _reply(context, conversation, null, true); @@ -132,9 +132,9 @@ class _ConversationDetailsScreenState extends State { ); } - Widget _body(BuildContext context, AsyncSnapshot snapshot) { + Widget _body(BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) return _errorState(context); - if (snapshot.hasData) return _successState(context, snapshot.data); + if (snapshot.hasData) return _successState(context, snapshot.data!); return LoadingIndicator(); } @@ -148,7 +148,7 @@ class _ConversationDetailsScreenState extends State { Widget _successState(BuildContext context, Conversation conversation) { return Container( - color: ParentTheme.of(context).nearSurfaceColor, + color: ParentTheme.of(context)?.nearSurfaceColor, child: RefreshIndicator( onRefresh: () { setState(() { @@ -161,7 +161,7 @@ class _ConversationDetailsScreenState extends State { itemCount: conversation.messages?.length ?? 0, separatorBuilder: (context, index) => SizedBox(height: 12), itemBuilder: (context, index) { - var message = conversation.messages[index]; + var message = conversation.messages![index]; return _message(context, conversation, message, index); }, ), @@ -177,35 +177,26 @@ class _ConversationDetailsScreenState extends State { }, child: Slidable( key: Key('message-${message.id}'), - actionPane: SlidableDrawerActionPane(), - secondaryActions: [ - IconSlideAction( - caption: L10n(context).replyAll, - color: ParentTheme.of(context).isDarkMode ? ParentColors.tiara : ParentColors.oxford, - foregroundColor: Theme.of(context).accentIconTheme.color, - iconWidget: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Icon( - CanvasIconsSolid.reply_all_2, - color: Theme.of(context).accentIconTheme.color, + endActionPane: ActionPane( + motion: const DrawerMotion(), + extentRatio: 0.5, + children: [ + SlidableAction( + label: L10n(context).replyAll, + backgroundColor: ParentTheme.of(context)?.isDarkMode == true ? ParentColors.tiara : ParentColors.oxford, + foregroundColor: ParentTheme.of(context)?.isDarkMode == true ? Colors.black : Colors.white, + icon: CanvasIconsSolid.reply_all_2, + onPressed: (context) => _reply(context, conversation, message, true), ), - ), - onTap: () => _reply(context, conversation, message, true), - ), - IconSlideAction( - caption: L10n(context).reply, - color: Theme.of(context).accentColor, - foregroundColor: Theme.of(context).accentIconTheme.color, - iconWidget: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Icon( - CanvasIconsSolid.reply, - color: Theme.of(context).accentIconTheme.color, + SlidableAction( + label: L10n(context).reply, + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: ParentTheme.of(context)?.isDarkMode == true ? Colors.black : Colors.white, + icon: CanvasIconsSolid.reply, + onPressed: (context) => _reply(context, conversation, message, false), ), - ), - onTap: () => _reply(context, conversation, message, false), - ), - ], + ], + ), child: MessageWidget( conversation: conversation, message: message, @@ -219,7 +210,7 @@ class _ConversationDetailsScreenState extends State { ); } - Future _reply(BuildContext context, Conversation conversation, Message message, bool replyAll) async { + Future _reply(BuildContext context, Conversation? conversation, Message? message, bool replyAll) async { var newConversation = await _interactor.addReply(context, conversation, message, replyAll); if (newConversation != null) { _hasBeenUpdated = true; diff --git a/apps/flutter_parent/lib/screens/inbox/conversation_details/message_widget.dart b/apps/flutter_parent/lib/screens/inbox/conversation_details/message_widget.dart index 37d025ea7c..329723032c 100644 --- a/apps/flutter_parent/lib/screens/inbox/conversation_details/message_widget.dart +++ b/apps/flutter_parent/lib/screens/inbox/conversation_details/message_widget.dart @@ -30,18 +30,18 @@ import 'package:flutter_parent/utils/style_slicer.dart'; import 'package:intl/intl.dart'; class MessageWidget extends StatefulWidget { - final Conversation conversation; - final Message message; - final String currentUserId; - final Function(Attachment) onAttachmentClicked; + final Conversation? conversation; + final Message? message; + final String? currentUserId; + final Function(Attachment)? onAttachmentClicked; const MessageWidget({ - Key key, - @required this.conversation, - @required this.message, - @required this.currentUserId, + this.conversation, + required this.message, + required this.currentUserId, this.onAttachmentClicked = null, - }) : super(key: key); + super.key, + }); @override _MessageWidgetState createState() => _MessageWidgetState(); @@ -52,11 +52,12 @@ class _MessageWidgetState extends State { @override Widget build(BuildContext context) { - var author = widget.conversation.participants.firstWhere( - (it) => it.id == widget.message.authorId, + var author = widget.conversation?.participants?.firstWhere( + (it) => it.id == widget.message?.authorId, orElse: () => BasicUser((b) => b..name = L10n(context).unknownUser), ); - var date = widget.message.createdAt.l10nFormat(L10n(context).dateAtTime); + var date = widget.message?.createdAt.l10nFormat(L10n(context).dateAtTime); + if (author == null || date == null) return Container(); return Container( padding: EdgeInsets.symmetric(vertical: 16), color: Theme.of(context).scaffoldBackgroundColor, @@ -67,7 +68,7 @@ class _MessageWidgetState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Linkify( - text: widget.message.body, + text: widget.message?.body ?? '', options: LinkifyOptions(humanize: false), onOpen: (link) => locator().routeInternally(context, link.url), ), @@ -83,9 +84,9 @@ class _MessageWidgetState extends State { key: Key('message-header'), child: InkWell( onTap: - widget.message.participatingUserIds.length > 1 // Only allow expansion if there are non-author participants - ? () => setState(() => _participantsExpanded = !_participantsExpanded) - : null, + widget.message?.participatingUserIds != null && widget.message!.participatingUserIds!.length > 1 // Only allow expansion if there are non-author participants + ? () => setState(() => _participantsExpanded = !_participantsExpanded) + : null, child: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Column( @@ -99,9 +100,9 @@ class _MessageWidgetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _authorText(context, widget.conversation, widget.message, author), + if (widget.message != null) _authorText(context, widget.conversation, widget.message!, author), SizedBox(height: 2), - Text(date, key: Key('message-date'), style: Theme.of(context).textTheme.subtitle2), + Text(date, key: Key('message-date'), style: Theme.of(context).textTheme.titleSmall), ], ), ), @@ -117,10 +118,10 @@ class _MessageWidgetState extends State { } Widget _participants(BasicUser author) { - var participants = widget.message.participatingUserIds - .map((id) => widget.conversation.participants.firstWhere((it) => it.id == id)) + var participants = widget.message!.participatingUserIds! + .map((id) => widget.conversation?.participants!.firstWhere((it) => it.id == id)) .toList() - ..retainWhere((it) => it.id != author.id); + ..retainWhere((it) => it?.id != author.id); return Padding( key: Key('participants'), padding: const EdgeInsetsDirectional.only(top: 16, start: 52), @@ -132,12 +133,12 @@ class _MessageWidgetState extends State { var user = participants[index]; return Row( children: [ - Avatar(user.avatarUrl, name: user.name, radius: 16), + Avatar(user?.avatarUrl, name: user?.name ?? '', radius: 16), SizedBox(width: 12), Expanded( - child: Text(user.name, - key: ValueKey('participant_id_${user.id}'), - style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 14))) + child: Text(user?.name ?? '', + key: ValueKey('participant_id_${user?.id}'), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontSize: 14))) ], ); }, @@ -145,24 +146,24 @@ class _MessageWidgetState extends State { ); } - Widget _authorText(BuildContext context, Conversation conversation, Message message, BasicUser author) { - String authorInfo; + Widget _authorText(BuildContext context, Conversation? conversation, Message message, BasicUser author) { + String authorInfo = ''; List slicers = []; - Color authorColor = ParentTheme.of(context).onSurfaceColor; + Color? authorColor = ParentTheme.of(context)?.onSurfaceColor; if (message.authorId == widget.currentUserId) { - var authorName = toBeginningOfSentenceCase(L10n(context).userNameMe); + var authorName = toBeginningOfSentenceCase(L10n(context).userNameMe) ?? ''; slicers.add(PatternSlice(authorName, style: TextStyle(color: authorColor), maxMatches: 1)); - if (message.participatingUserIds.length == 2) { - var otherUser = conversation.participants.firstWhere( + if (message.participatingUserIds!.length == 2) { + var otherUser = conversation?.participants?.firstWhere( (it) => it.id != message.authorId, orElse: () => BasicUser((b) => b..name = L10n(context).unknownUser), ); - var recipientName = UserName.fromBasicUser(otherUser).text; + var recipientName = UserName.fromBasicUser(otherUser!).text; slicers.add(PronounSlice(otherUser.pronouns)); authorInfo = L10n(context).authorToRecipient(authorName, recipientName); - } else if (message.participatingUserIds.length > 2) { - authorInfo = L10n(context).authorToNOthers(authorName, message.participatingUserIds.length - 1); + } else if (message.participatingUserIds!.length > 2) { + authorInfo = L10n(context).authorToNOthers(authorName, message.participatingUserIds!.length - 1); } else { authorInfo = authorName; } @@ -171,13 +172,13 @@ class _MessageWidgetState extends State { String authorName = UserName.fromBasicUser(author).text; slicers.add(PatternSlice(authorName, style: TextStyle(color: authorColor), maxMatches: 1)); slicers.add(PronounSlice(author.pronouns)); - if (message.participatingUserIds.length == 2) { + if (message.participatingUserIds!.length == 2) { authorInfo = L10n(context).authorToRecipient(authorName, L10n(context).userNameMe); - } else if (message.participatingUserIds.length > 2) { + } else if (message.participatingUserIds!.length > 2) { authorInfo = L10n(context).authorToRecipientAndNOthers( authorName, L10n(context).userNameMe, - message.participatingUserIds.length - 2, + message.participatingUserIds!.length - 2, ); } else { authorInfo == authorName; @@ -185,14 +186,14 @@ class _MessageWidgetState extends State { } return Text.rich( - StyleSlicer.apply(authorInfo, slicers, baseStyle: Theme.of(context).textTheme.caption), + StyleSlicer.apply(authorInfo, slicers, baseStyle: Theme.of(context).textTheme.bodySmall), key: Key('author-info'), ); } - Widget _attachmentsWidget(BuildContext context, Message message) { - List attachments = message.attachments?.toList() ?? []; - if (message.mediaComment != null) attachments.add(message.mediaComment.toAttachment()); + Widget _attachmentsWidget(BuildContext context, Message? message) { + List attachments = message?.attachments?.toList() ?? []; + if (message?.mediaComment != null) attachments.add(message!.mediaComment!.toAttachment()); if (attachments.isEmpty) return Container(); return Container( height: 108, diff --git a/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_interactor.dart b/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_interactor.dart index dc6be093ee..cc1b5a150c 100644 --- a/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_interactor.dart +++ b/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_interactor.dart @@ -12,6 +12,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:collection/collection.dart'; import 'package:flutter_parent/models/conversation.dart'; import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/models/enrollment.dart'; @@ -24,14 +25,14 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:tuple/tuple.dart'; class ConversationListInteractor { - Future> getConversations({bool forceRefresh: false}) async { + Future> getConversations({bool forceRefresh = false}) async { var api = locator(); try { // Get messages from both 'normal' scope 'sent' scopes - var results = await Future.wait([ + var results = (await Future.wait([ api.getConversations(forceRefresh: forceRefresh), api.getConversations(scope: 'sent', forceRefresh: forceRefresh) - ]); + ])).nonNulls.toList(); // Remove messages in the 'sent' scope that also exist in the normal scope results[1].retainWhere((sent) => !results[0].any((it) => it.id == sent.id)); @@ -41,8 +42,8 @@ class ConversationListInteractor { // Sort by date (descending) conversations.sort((a, b) { - var dateA = a.lastMessageAt ?? a.lastAuthoredMessageAt; - var dateB = b.lastMessageAt ?? b.lastAuthoredMessageAt; + var dateA = a.lastMessageAt ?? a.lastAuthoredMessageAt ?? DateTime.now(); + var dateB = b.lastMessageAt ?? b.lastAuthoredMessageAt ?? DateTime.now(); return dateB.compareTo(dateA); }); return Future.value(conversations); @@ -51,11 +52,11 @@ class ConversationListInteractor { } } - Future> getCoursesForCompose() async { + Future?> getCoursesForCompose() async { return locator().getObserveeCourses(); } - Future> getStudentEnrollments() async { + Future?> getStudentEnrollments() async { return locator().getObserveeEnrollments(); } @@ -66,16 +67,16 @@ class ConversationListInteractor { enrollments.retainWhere((e) => e.observedUser != null); List> thing = enrollments .map((e) { - final course = courses.firstWhere((c) => c.id == e.courseId, orElse: () => null); + final course = courses.firstWhereOrNull((c) => c.id == e.courseId); if (course == null) return null; - return Tuple2(e.observedUser, course); + return Tuple2(e.observedUser!, course); }) - .where((e) => e != null) + .nonNulls .toList(); // Sort users in alphabetical order and sort their courses alphabetically - thing.sortBy( - [(it) => it.item1?.shortName, (it) => it.item2.name], + thing.sortBySelector( + [(it) => it?.item1.shortName, (it) => it?.item2.name], ); return thing; diff --git a/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_screen.dart b/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_screen.dart index 758320f3c5..85cb5c8b13 100644 --- a/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_screen.dart +++ b/apps/flutter_parent/lib/screens/inbox/conversation_list/conversation_list_screen.dart @@ -46,7 +46,7 @@ class ConversationListScreen extends StatefulWidget { } class ConversationListState extends State { - Future> _conversationsFuture; + late Future> _conversationsFuture; final GlobalKey _refreshIndicatorKey = new GlobalKey(); ConversationListInteractor interactor = locator(); @@ -63,7 +63,7 @@ class ConversationListState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(L10n(context).inbox), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: FutureBuilder( future: _conversationsFuture, @@ -72,8 +72,8 @@ class ConversationListState extends State { if (snapshot.hasError) { body = _errorState(context); } else if (snapshot.hasData) { - if (snapshot.data.isNotEmpty) { - body = _successState(context, snapshot.data); + if (snapshot.data!.isNotEmpty) { + body = _successState(context, snapshot.data!); } else { body = _emptyState(context); } @@ -106,7 +106,7 @@ class ConversationListState extends State { Widget _errorState(BuildContext context) { return ErrorPandaWidget(L10n(context).errorLoadingMessages, () { - _refreshIndicatorKey.currentState.show(); + _refreshIndicatorKey.currentState?.show(); }); } @@ -139,7 +139,7 @@ class ConversationListState extends State { ), Text( _formatMessageDate(item.lastMessageAt ?? item.lastAuthoredMessageAt), - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.bodySmall, ), ], ), @@ -149,14 +149,14 @@ class ConversationListState extends State { SizedBox(height: 2), if (item.contextName?.isNotEmpty == true) Text( - item.contextName, + item.contextName!, style: TextStyle(fontWeight: FontWeight.w500), key: ValueKey('conversation_context_$index'), ), SizedBox(height: 4), Text( - item.lastMessage ?? item.lastAuthoredMessage, - style: Theme.of(context).textTheme.bodyText2, + item.lastMessage ?? item.lastAuthoredMessage ?? '', + style: Theme.of(context).textTheme.bodyMedium, maxLines: 2, key: ValueKey('conversation_message_$index'), ), @@ -171,7 +171,7 @@ class ConversationListState extends State { courseName: item.contextName, ), ); - if (refresh == true || item.isUnread()) _refreshIndicatorKey.currentState.show(); + if (refresh == true || item.isUnread()) _refreshIndicatorKey.currentState?.show(); }, ); return item.isUnread() ? WidgetBadge(tile) : tile; @@ -187,7 +187,7 @@ class ConversationListState extends State { ); } - String _formatMessageDate(DateTime date) { + String _formatMessageDate(DateTime? date) { if (date == null) return ''; date = date.toLocal(); var format = DateFormat.MMM(supportedDateLocale).add_d(); @@ -204,7 +204,7 @@ class ConversationListState extends State { Widget avatar; var users = conversation.participants?.toList() ?? []; - users.retainWhere((user) => conversation.audience.contains(user.id)); + users.retainWhere((user) => conversation.audience?.contains(user.id) == true); if (users.length == 2) { avatar = SizedBox( @@ -248,7 +248,7 @@ class ConversationListState extends State { builder: (context) { return FutureBuilder( future: Future.wait([coursesFuture, studentsFuture]) - .then((response) => _CoursesAndStudents(response[0], response[1])), + .then((response) => _CoursesAndStudents(response[0] as List, response[1] as List)), builder: (BuildContext context, AsyncSnapshot<_CoursesAndStudents> snapshot) { if (snapshot.hasError) { return Padding( @@ -275,10 +275,10 @@ class ConversationListState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Text( L10n(context).messageChooseCourse, - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.bodySmall, ), ), - ..._courseList(snapshot.data), + ..._courseList(snapshot.data!), ], ); } else { @@ -304,10 +304,10 @@ class ConversationListState extends State { Course course = t.item2; var w = ListTile( title: Text(course.name, key: ValueKey('course_list_course_${course.id}')), - subtitle: Text(L10n(context).courseForWhom(user.shortName)), + subtitle: Text(L10n(context).courseForWhom(user.shortName!)), onTap: () async { String postscript = L10n(context).messageLinkPostscript( - user.shortName, + user.shortName!, '${ApiPrefs.getDomain()}/courses/${course.id}', ); Navigator.pop(context); // Dismisses the bottom sheet @@ -319,7 +319,7 @@ class ConversationListState extends State { course.name, postscript, )); - if (refresh == true) _refreshIndicatorKey.currentState.show(); + if (refresh == true) _refreshIndicatorKey.currentState?.show(); }, ); widgets.add(w); diff --git a/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_interactor.dart b/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_interactor.dart index 347740c0d6..1a3278d89e 100644 --- a/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_interactor.dart +++ b/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_interactor.dart @@ -25,21 +25,21 @@ import 'package:flutter_parent/utils/service_locator.dart'; import '../attachment_utils/attachment_picker.dart'; class CreateConversationInteractor { - Future loadData(String courseId, String studentId) async { + Future loadData(String courseId, String? studentId) async { final courseFuture = locator().getCourse(courseId); final recipientFuture = locator().getRecipients(courseId); final permissionsFuture = locator().getCoursePermissions(courseId); final permissions = await permissionsFuture; final recipients = await recipientFuture; - final userId = ApiPrefs.getUser().id; + final userId = ApiPrefs.getUser()?.id; - recipients.retainWhere((recipient) { + recipients?.retainWhere((recipient) { // Allow self and specified student as recipients if the sendMessages permission is granted - if (permissions.sendMessages == true && (recipient.id == studentId || recipient.id == userId)) return true; + if (permissions?.sendMessages == true && (recipient.id == studentId || recipient.id == userId)) return true; // Always allow instructors (teachers and TAs) as recipients - var enrollments = recipient.commonCourses[courseId]; + var enrollments = recipient.commonCourses?[courseId]; if (enrollments == null) return false; return enrollments.contains('TeacherEnrollment') || enrollments.contains('TaEnrollment'); }); @@ -47,7 +47,7 @@ class CreateConversationInteractor { return CreateConversationData(await courseFuture, recipients); } - Future createConversation( + Future createConversation( String courseId, List recipientIds, String subject, @@ -57,14 +57,14 @@ class CreateConversationInteractor { return locator().createConversation(courseId, recipientIds, subject, body, attachmentIds); } - Future addAttachment(BuildContext context) async { + FutureaddAttachment(BuildContext context) async { return AttachmentPicker.asBottomSheet(context); } } class CreateConversationData { - final Course course; - final List recipients; + final Course? course; + final List? recipients; CreateConversationData(this.course, this.recipients); } diff --git a/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_screen.dart b/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_screen.dart index 1cdbf7b6e8..0ca0ffe4dc 100644 --- a/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_screen.dart +++ b/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_screen.dart @@ -40,14 +40,12 @@ class CreateConversationScreen extends StatefulWidget { this.studentId, this.subjectTemplate, this.postscript, - ) : assert(courseId != null), - assert(studentId != null), - assert(subjectTemplate != null); + ); final String courseId; - final String studentId; + final String? studentId; final String subjectTemplate; - final String postscript; + final String? postscript; static final sendKey = Key('sendButton'); static final attachmentKey = Key('attachmentButton'); @@ -66,13 +64,13 @@ class _CreateConversationScreenState extends State wit String _subjectText = ''; String _bodyText = ''; - TextEditingController _subjectController; + late TextEditingController _subjectController; TextEditingController _bodyController = TextEditingController(); List _allRecipients = []; List _selectedRecipients = []; List _attachments = []; - Course course; + Course? course; bool _loading = false; bool _error = false; @@ -124,10 +122,10 @@ class _CreateConversationScreenState extends State wit }); _interactor.loadData(widget.courseId, widget.studentId).then((data) { course = data.course; - _allRecipients = data.recipients; + _allRecipients = data.recipients ?? []; String courseId = widget.courseId; _selectedRecipients = - _allRecipients.where((it) => it.commonCourses[courseId]?.contains('TeacherEnrollment')).toList(); + _allRecipients.where((it) => it.commonCourses?[courseId]?.contains('TeacherEnrollment') == true).toList(); setState(() { _loading = false; }); @@ -144,7 +142,7 @@ class _CreateConversationScreenState extends State wit setState(() => _sending = true); try { var recipientIds = _selectedRecipients.map((it) => it.id).toList(); - var attachmentIds = _attachments.map((it) => it.attachment.id).toList(); + var attachmentIds = _attachments.map((it) => it.attachment!.id).toList(); if (widget.postscript != null) { _bodyText += '\n\n${widget.postscript}'; } @@ -154,26 +152,26 @@ class _CreateConversationScreenState extends State wit .showSnackBar(SnackBar(content: Text(L10n(context).messageSent))); } catch (e) { setState(() => _sending = false); - _scaffoldKey.currentState.showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n(context).errorSendingMessage)), ); } } - Future _onWillPop(BuildContext context) { + Future _onWillPop(BuildContext context) async { if (_sending) return Future.value(false); if (_bodyText.isEmpty && _attachments.isEmpty) return Future.value(true); - return showDialog( + return await showDialog( context: context, - builder: (context) => new AlertDialog( + builder: (Bucontext) => new AlertDialog( title: new Text(L10n(context).unsavedChangesDialogTitle), content: new Text(L10n(context).unsavedChangesDialogBody), actions: [ - new FlatButton( + new TextButton( onPressed: () => Navigator.of(context).pop(false), child: new Text(L10n(context).no), ), - new FlatButton( + new TextButton( onPressed: () { _attachments.forEach((it) => it.deleteAttachment()); Navigator.of(context).pop(true); @@ -203,15 +201,15 @@ class _CreateConversationScreenState extends State wit ); } - Widget _appBar(BuildContext context) { + AppBar _appBar(BuildContext context) { return AppBar( - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), title: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(L10n(context).newMessageTitle), - Text(course?.courseCode ?? '', style: Theme.of(context).textTheme.caption), + Text(course?.courseCode ?? '', style: Theme.of(context).textTheme.bodySmall), ], ), actions: [ @@ -252,8 +250,8 @@ class _CreateConversationScreenState extends State wit tooltip: L10n(context).sendMessage, key: CreateConversationScreen.sendKey, icon: Icon(Icons.send), - color: Theme.of(context).accentColor, - disabledColor: Theme.of(context).iconTheme.color.withOpacity(0.25), + color: Theme.of(context).colorScheme.secondary, + disabledColor: Theme.of(context).iconTheme.color?.withOpacity(0.25), onPressed: _canSend() ? _send : null, ) ], @@ -327,20 +325,22 @@ class _CreateConversationScreenState extends State wit child: Text( L10n(context).errorLoadingRecipients, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16), + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16), ), ), - FlatButton( + TextButton( onPressed: _loadRecipients, child: Text( L10n(context).retry, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16), + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16), ), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(24.0), - side: BorderSide(color: ParentColors.tiara), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(24.0), + side: BorderSide(color: ParentColors.tiara), + ), ), - ) + ), ], ), ), @@ -352,7 +352,6 @@ class _CreateConversationScreenState extends State wit Widget _recipientsWidget(BuildContext context) { return AnimatedSize( - vsync: this, alignment: Alignment.topLeft, curve: Curves.easeInOutBack, duration: Duration(milliseconds: 350), @@ -398,7 +397,7 @@ class _CreateConversationScreenState extends State wit return [ Chip( label: Text(L10n(context).noRecipientsSelected), - backgroundColor: ParentTheme.of(context).nearSurfaceColor, + backgroundColor: ParentTheme.of(context)?.nearSurfaceColor, avatar: Icon( CanvasIcons.warning, color: Colors.redAccent, @@ -420,7 +419,7 @@ class _CreateConversationScreenState extends State wit } } - Widget _chip(Recipient user, {bool ellipsize: false}) { + Widget _chip(Recipient user, {bool ellipsize = false}) { return Chip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, key: ValueKey("user_chip_${user.id}"), @@ -448,7 +447,7 @@ class _CreateConversationScreenState extends State wit ), ), avatar: Avatar(user.avatarUrl, name: user.name), - backgroundColor: ParentTheme.of(context).nearSurfaceColor, + backgroundColor: ParentTheme.of(context)?.nearSurfaceColor, ); } @@ -460,7 +459,7 @@ class _CreateConversationScreenState extends State wit key: CreateConversationScreen.subjectKey, controller: _subjectController, enabled: !_sending, - style: Theme.of(context).textTheme.bodyText1, + style: Theme.of(context).textTheme.bodyLarge, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: L10n(context).messageSubjectInputHint, @@ -480,9 +479,8 @@ class _CreateConversationScreenState extends State wit controller: _bodyController, enabled: !_sending, textCapitalization: TextCapitalization.sentences, - minLines: 4, maxLines: null, - style: Theme.of(context).textTheme.bodyText2, + // style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( labelText: L10n(context).messageBodyInputHint, contentPadding: EdgeInsets.all(16), @@ -504,7 +502,7 @@ class _CreateConversationScreenState extends State wit padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), child: Text( L10n(context).recipients, - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), ), Expanded( @@ -523,7 +521,7 @@ class _CreateConversationScreenState extends State wit name: user.name, overlay: selected ? Container( - color: Theme.of(context).accentColor.withOpacity(0.8), + color: Theme.of(context).colorScheme.secondary.withOpacity(0.8), child: Icon(Icons.check, color: Colors.white), ) : null, @@ -550,7 +548,7 @@ class _CreateConversationScreenState extends State wit } String _enrollmentType(BuildContext context, Recipient user) { - var type = user.commonCourses[widget.courseId].first; + var type = user.commonCourses?[widget.courseId]?.first; switch (type) { case 'TeacherEnrollment': return L10n(context).enrollmentTypeTeacher; @@ -567,9 +565,9 @@ class _CreateConversationScreenState extends State wit } class AttachmentWidget extends StatelessWidget { - AttachmentWidget({Key key, this.onDelete}) : super(key: key); + AttachmentWidget({this.onDelete, super.key}); - final Function(AttachmentHandler) onDelete; + final Function(AttachmentHandler)? onDelete; @override Widget build(BuildContext context) { @@ -616,12 +614,12 @@ class AttachmentWidget extends StatelessWidget { height: 48, child: CircularProgressIndicator( value: handler.progress, - backgroundColor: ParentTheme.of(context).nearSurfaceColor, + backgroundColor: ParentTheme.of(context)?.nearSurfaceColor, ), ), Text( handler.progress == null ? '' : NumberFormat.percentPattern().format(handler.progress), - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + style: Theme.of(context).textTheme.bodySmall, ) ], ), @@ -642,22 +640,24 @@ class AttachmentWidget extends StatelessWidget { child: Text(L10n(context).delete), ), ], - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 6), - Icon( - CanvasIcons.warning, - size: 27, - color: Colors.red, - ), - SizedBox(height: 15), - Text( - L10n(context).attachmentFailed, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12), - ), - ], + child: Align( + alignment: Alignment.center, + child: ListView( + children: [ + SizedBox(height: 6), + Icon( + CanvasIcons.warning, + size: 27, + color: Colors.red, + ), + SizedBox(height: 15), + Text( + L10n(context).attachmentFailed, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12), + ), + ], + ), ), ); } @@ -679,13 +679,13 @@ class AttachmentWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - handler.attachment.getIcon(), - color: Theme.of(context).accentColor, + handler.attachment?.getIcon(), + color: Theme.of(context).colorScheme.secondary, ), Padding( padding: const EdgeInsets.fromLTRB(12, 11, 12, 0), child: Text( - handler.attachment.displayName, + handler.attachment?.displayName ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -694,13 +694,13 @@ class AttachmentWidget extends StatelessWidget { ) ], ), - if (handler.attachment?.thumbnailUrl != null && handler.attachment.thumbnailUrl.isNotEmpty) + if (handler.attachment?.thumbnailUrl != null && handler.attachment?.thumbnailUrl?.isNotEmpty == true) ClipRRect( borderRadius: new BorderRadius.circular(4), child: FadeInImage.memoryNetwork( fadeInDuration: const Duration(milliseconds: 300), fit: BoxFit.cover, - image: handler.attachment.thumbnailUrl, + image: handler.attachment!.thumbnailUrl!, placeholder: kTransparentImage, ), ), @@ -716,7 +716,7 @@ class AttachmentWidget extends StatelessWidget { switch (option) { case 'delete': handler.deleteAttachment(); - if (onDelete != null) onDelete(handler); + if (onDelete != null) onDelete!(handler); break; case 'retry': handler.performUpload(); diff --git a/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_interactor.dart b/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_interactor.dart index 5592d03571..a5e23d76de 100644 --- a/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_interactor.dart +++ b/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_interactor.dart @@ -25,60 +25,60 @@ import 'package:flutter_parent/utils/service_locator.dart'; import '../attachment_utils/attachment_picker.dart'; class ConversationReplyInteractor { - Future createReply( - Conversation conversation, - Message message, + Future createReply( + Conversation? conversation, + Message? message, String body, List attachmentIds, bool replyAll, ) async { - Message replyMessage = message ?? conversation.messages[0]; + Message replyMessage = message ?? conversation!.messages![0]; List includedMessageIds = [if (message != null || !replyAll) replyMessage.id]; List recipientIds = []; if (!replyAll) { if (replyMessage.authorId == getCurrentUserId()) { - recipientIds = replyMessage.participatingUserIds.toList(); + recipientIds = replyMessage.participatingUserIds!.toList(); } else { recipientIds = [replyMessage.authorId]; } } else { // We need to make sure the recipients list doesn't contain any off limit users, such as non-observed students. - final courseId = conversation.getContextId(); - final userId = ApiPrefs.getUser().id; + final courseId = conversation?.getContextId(); + final userId = ApiPrefs.getUser()?.id; final enrollments = await locator().getObserveeEnrollments(); final observeeIds = enrollments - .map((enrollment) => enrollment.observedUser) + ?.map((enrollment) => enrollment.observedUser) .where((student) => student != null) .toSet() - .map((student) => student.id); - final permissions = await locator().getCoursePermissions(courseId); + .map((student) => student!.id); + final permissions = await locator().getCoursePermissions(courseId!); final recipients = await locator().getRecipients(courseId); - recipients.retainWhere((recipient) { + recipients?.retainWhere((recipient) { // Allow self and any observed students as recipients if the sendMessages permission is granted - if (permissions.sendMessages == true && (observeeIds.contains(recipient.id) || recipient.id == userId)) + if (permissions?.sendMessages == true && (observeeIds?.contains(recipient.id) == true || recipient.id == userId)) return true; // Always allow instructors (teachers and TAs) as recipients - var enrollments = recipient.commonCourses[courseId]; + var enrollments = recipient.commonCourses![courseId]; if (enrollments == null) return false; return enrollments.contains('TeacherEnrollment') || enrollments.contains('TaEnrollment'); }); - final filteredRecipientIds = recipients.map((recipient) => recipient.id); + final filteredRecipientIds = recipients?.map((recipient) => recipient.id); - recipientIds = replyMessage.participatingUserIds + recipientIds = replyMessage.participatingUserIds! .toList() - .where((participantId) => filteredRecipientIds.contains(participantId)) + .where((participantId) => filteredRecipientIds?.contains(participantId) == true) .toList(); } - return locator().addMessage(conversation.id, body, recipientIds, attachmentIds, includedMessageIds); + return locator().addMessage(conversation?.id, body, recipientIds, attachmentIds, includedMessageIds); } - Future addAttachment(BuildContext context) async { + Future addAttachment(BuildContext context) async { return AttachmentPicker.asBottomSheet(context); } - String getCurrentUserId() => ApiPrefs.getUser().id; + String getCurrentUserId() => ApiPrefs.getUser()!.id; } diff --git a/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_screen.dart b/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_screen.dart index a9e73ff6d8..7abc036d6a 100644 --- a/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_screen.dart +++ b/apps/flutter_parent/lib/screens/inbox/reply/conversation_reply_screen.dart @@ -34,8 +34,8 @@ import 'conversation_reply_interactor.dart'; class ConversationReplyScreen extends StatefulWidget { ConversationReplyScreen(this.conversation, this.message, this.replyAll); - final Conversation conversation; - final Message message; + final Conversation? conversation; + final Message? message; final bool replyAll; static final sendKey = Key('sendButton'); @@ -77,7 +77,7 @@ class _ConversationReplyScreenState extends State { _send() async { setState(() => _sending = true); - var attachmentIds = _attachments.map((it) => it.attachment.id).toList(); + var attachmentIds = _attachments.map((it) => it.attachment?.id).toList().nonNulls.toList(); try { var result = await _interactor.createReply( widget.conversation, @@ -86,31 +86,32 @@ class _ConversationReplyScreenState extends State { attachmentIds, widget.replyAll, ); - var newMessage = result.messages[0]; - var updatedConversation = widget.conversation.rebuild((c) => c..messages.insert(0, newMessage)); + var newMessage = result?.messages?.first; + Conversation? updatedConversation = null; + if (newMessage != null) updatedConversation = widget.conversation?.rebuild((c) => c..messages.insert(0, newMessage)); Navigator.of(context).pop(updatedConversation); // Return updated conversation } catch (e) { setState(() => _sending = false); - _scaffoldKey.currentState.showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n(context).errorSendingMessage)), ); } } - Future _onWillPop() { + Future _onWillPop() async { if (_sending) return Future.value(false); if (_bodyText.isEmpty && _attachments.isEmpty) return Future.value(true); - return showDialog( + return await showDialog( context: context, builder: (context) => new AlertDialog( title: new Text(L10n(context).unsavedChangesDialogTitle), content: new Text(L10n(context).unsavedChangesDialogBody), actions: [ - new FlatButton( + new TextButton( onPressed: () => Navigator.of(context).pop(false), child: new Text(L10n(context).no), ), - new FlatButton( + new TextButton( onPressed: () { _attachments.forEach((it) => it.deleteAttachment()); Navigator.of(context).pop(true); @@ -140,15 +141,15 @@ class _ConversationReplyScreenState extends State { ); } - Widget _appBar(BuildContext context) { + AppBar _appBar(BuildContext context) { return AppBar( - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), title: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(widget.replyAll ? L10n(context).replyAll : L10n(context).reply), - Text(widget.conversation.subject, style: Theme.of(context).textTheme.caption), + Text(widget.replyAll ? L10n(context).replyAll : L10n(context).reply, style: Theme.of(context).textTheme.bodySmall), + Text(widget.conversation?.subject ?? '', style: Theme.of(context).textTheme.bodySmall), ], ), actions: [ @@ -189,8 +190,8 @@ class _ConversationReplyScreenState extends State { tooltip: L10n(context).sendMessage, key: ConversationReplyScreen.sendKey, icon: Icon(Icons.send), - color: Theme.of(context).accentColor, - disabledColor: Theme.of(context).iconTheme.color.withOpacity(0.25), + color: Theme.of(context).colorScheme.secondary, + disabledColor: Theme.of(context).iconTheme.color?.withOpacity(0.25), onPressed: _canSend() ? _send : null, ) ], @@ -204,7 +205,7 @@ class _ConversationReplyScreenState extends State { children: [ MessageWidget( conversation: widget.conversation, - message: widget.message ?? widget.conversation.messages[0], + message: widget.message ?? widget.conversation?.messages?[0], currentUserId: _interactor.getCurrentUserId(), onAttachmentClicked: (attachment) { locator().push(context, ViewAttachmentScreen(attachment)); @@ -260,7 +261,7 @@ class _ConversationReplyScreenState extends State { textCapitalization: TextCapitalization.sentences, minLines: 4, maxLines: null, - style: Theme.of(context).textTheme.bodyText2, + style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( hintText: L10n(context).messageBodyInputHint, contentPadding: EdgeInsets.all(16), diff --git a/apps/flutter_parent/lib/screens/login_landing_screen.dart b/apps/flutter_parent/lib/screens/login_landing_screen.dart index af81ff724f..941e66ac4e 100644 --- a/apps/flutter_parent/lib/screens/login_landing_screen.dart +++ b/apps/flutter_parent/lib/screens/login_landing_screen.dart @@ -88,10 +88,11 @@ class LoginLandingScreen extends StatelessWidget { Widget _body(BuildContext context) { final lastLoginAccount = ApiPrefs.getLastAccount(); - final assetString = ParentTheme.of(context).isDarkMode ? 'assets/svg/canvas-parent-login-logo-dark.svg' : 'assets/svg/canvas-parent-login-logo.svg'; + final assetString = ParentTheme.of(context)?.isDarkMode == true ? 'assets/svg/canvas-parent-login-logo-dark.svg' : 'assets/svg/canvas-parent-login-logo.svg'; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Spacer(), SvgPicture.asset( @@ -107,9 +108,9 @@ class LoginLandingScreen extends StatelessWidget { _filledButton( context, lastLoginAccount.item1.name == null || - lastLoginAccount.item1.name.isEmpty + lastLoginAccount.item1.name!.isEmpty ? lastLoginAccount.item1.domain - : lastLoginAccount.item1.name, () { + : lastLoginAccount.item1.name!, () { onSavedSchoolPressed(context, lastLoginAccount); }), SizedBox(height: 16), @@ -153,6 +154,7 @@ class LoginLandingScreen extends StatelessWidget { width: min(parentWidth * 0.5, 400), child: Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Spacer(), if (lastLoginAccount == null) @@ -163,9 +165,9 @@ class LoginLandingScreen extends StatelessWidget { _filledButton( context, lastLoginAccount.item1.name == null || - lastLoginAccount.item1.name.isEmpty + lastLoginAccount.item1.name!.isEmpty ? lastLoginAccount.item1.domain - : lastLoginAccount.item1.name, () { + : lastLoginAccount.item1.name!, () { onSavedSchoolPressed(context, lastLoginAccount); }), SizedBox(height: 16), @@ -191,23 +193,23 @@ class LoginLandingScreen extends StatelessWidget { BuildContext context, String title, VoidCallback onPressed) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 48.0), - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - child: Padding( + child: FilledButton( + child: Padding ( padding: const EdgeInsets.all(16.0), child: Text( title, - style: TextStyle(fontSize: 16), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.white, fontSize: 16), overflow: TextOverflow.ellipsis, ), ), - color: Theme.of(context).accentColor, - textColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4))), - onPressed: onPressed, - ), + style: FilledButton.styleFrom( + textStyle: TextStyle(color: Colors.white), + backgroundColor: Theme.of(context).colorScheme.secondary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)) + ), + ), + onPressed: onPressed ), ); } @@ -216,25 +218,23 @@ class LoginLandingScreen extends StatelessWidget { BuildContext context, String title, VoidCallback onPressed) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 48.0), - child: ButtonTheme( - child: OutlinedButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - title, - style: Theme.of(context).textTheme.subtitle1, - ), + child: OutlinedButton( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, ), - style: OutlinedButton.styleFrom( - minimumSize: Size(double.infinity, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.0), - ), - side: BorderSide( - width: 1, color: ParentTheme.of(context).onSurfaceColor), + ), + style: OutlinedButton.styleFrom( + minimumSize: Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.0), ), - onPressed: onPressed, + side: BorderSide( + width: 1, color: ParentTheme.of(context)?.onSurfaceColor ?? Colors.transparent), ), + onPressed: onPressed, ), ); } @@ -260,7 +260,7 @@ class LoginLandingScreen extends StatelessWidget { SizedBox(width: 8), Text( L10n(context).loginWithQRCode, - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, ), ]), )); @@ -281,7 +281,7 @@ class LoginLandingScreen extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 48), child: Text(L10n(context).previousLogins, - style: Theme.of(context).textTheme.subtitle1), + style: Theme.of(context).textTheme.titleMedium), ), SizedBox(height: 6), Padding( @@ -306,7 +306,7 @@ class LoginLandingScreen extends StatelessWidget { context, PandaRouter.rootSplash()); }, leading: Stack( - overflow: Overflow.visible, + clipBehavior: Clip.none, children: [ Avatar.fromUser(login.currentUser), if (login.isMasquerading) @@ -329,8 +329,9 @@ class LoginLandingScreen extends StatelessWidget { ], ), title: UserName.fromUser(login.currentUser), - subtitle: Text(login.currentDomain, overflow: TextOverflow.ellipsis), + subtitle: Text(login.currentDomain, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall), trailing: IconButton( + color: Theme.of(context).textTheme.labelSmall?.color, tooltip: L10n(context).delete, onPressed: () async { await ApiPrefs.removeLogin(login); @@ -360,7 +361,7 @@ class LoginLandingScreen extends StatelessWidget { locator().pushRoute( context, PandaRouter.loginWeb(lastAccount.item1.domain, - accountName: lastAccount.item1.name, loginFlow: lastAccount.item2)); + accountName: lastAccount.item1.name!, loginFlow: lastAccount.item2)); } void _changeLoginFlow(BuildContext context) { @@ -382,8 +383,7 @@ class LoginLandingScreen extends StatelessWidget { break; } - _scaffoldKey.currentState.removeCurrentSnackBar(); - _scaffoldKey.currentState - .showSnackBar(SnackBar(content: Text(flowDescription))); + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(flowDescription))); } } diff --git a/apps/flutter_parent/lib/screens/manage_students/manage_students_interactor.dart b/apps/flutter_parent/lib/screens/manage_students/manage_students_interactor.dart index e05a9a31be..0d6d440453 100644 --- a/apps/flutter_parent/lib/screens/manage_students/manage_students_interactor.dart +++ b/apps/flutter_parent/lib/screens/manage_students/manage_students_interactor.dart @@ -18,17 +18,17 @@ import 'package:flutter_parent/network/api/enrollments_api.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class ManageStudentsInteractor { - Future> getStudents({bool forceRefresh = false}) async { + Future?> getStudents({bool forceRefresh = false}) async { var enrollments = await _enrollmentsApi().getObserveeEnrollments(forceRefresh: forceRefresh); - List users = filterStudents(enrollments); + List? users = filterStudents(enrollments); sortUsers(users); return users; } - List filterStudents(List enrollments) => - enrollments.map((enrollment) => enrollment.observedUser).where((student) => student != null).toSet().toList(); + List? filterStudents(List? enrollments) => + enrollments?.map((enrollment) => enrollment.observedUser).toSet().toList().nonNulls.toList(); - void sortUsers(List users) => users.sort((user1, user2) => user1.sortableName.compareTo(user2.sortableName)); + void sortUsers(List? users) => users?.sort((user1, user2) => (user1.sortableName ?? '').compareTo(user2.sortableName ?? '')); EnrollmentsApi _enrollmentsApi() => locator(); } diff --git a/apps/flutter_parent/lib/screens/manage_students/manage_students_screen.dart b/apps/flutter_parent/lib/screens/manage_students/manage_students_screen.dart index ae3834eb75..b57b9669c7 100644 --- a/apps/flutter_parent/lib/screens/manage_students/manage_students_screen.dart +++ b/apps/flutter_parent/lib/screens/manage_students/manage_students_screen.dart @@ -34,17 +34,17 @@ import 'manage_students_interactor.dart'; /// /// Pull to refresh and updating when a pairing code is used are handled, however. class ManageStudentsScreen extends StatefulWidget { - final List _students; + final List? _students; - ManageStudentsScreen(this._students, {Key key}) : super(key: key); + ManageStudentsScreen(this._students, {super.key}); @override State createState() => _ManageStudentsState(); } class _ManageStudentsState extends State { - Future> _studentsFuture; - Future> _loadStudents() => locator().getStudents(forceRefresh: true); + Future?>? _studentsFuture; + Future?> _loadStudents() => locator().getStudents(forceRefresh: true); GlobalKey _refreshKey = GlobalKey(); @@ -63,22 +63,22 @@ class _ManageStudentsState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(L10n(context).manageStudents), - bottom: ParentTheme.of(context).appBarDivider(), + bottom: ParentTheme.of(context)?.appBarDivider(), ), body: FutureBuilder( initialData: widget._students, future: _studentsFuture, - builder: (context, AsyncSnapshot> snapshot) { + builder: (context, AsyncSnapshot?> snapshot) { // No wait view - users should be passed in on init, and the refresh indicator should handle the pull to refresh // Get the view based on the state of the snapshot Widget view; if (snapshot.hasError) { view = _error(context); - } else if (snapshot.data == null || snapshot.data.isEmpty) { + } else if (snapshot.data == null || snapshot.data!.isEmpty) { view = _empty(context); } else { - view = _StudentsList(snapshot.data); + view = _StudentsList(snapshot.data!); } return RefreshIndicator( @@ -108,25 +108,25 @@ class _ManageStudentsState extends State { key: ValueKey('studentTextHero${students[index].id}'), child: UserName.fromUserShortName( students[index], - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, ), ), onTap: () async { var needsRefresh = await locator.get().push(context, AlertThresholdsScreen(students[index])); if (needsRefresh == true) { - _refreshKey.currentState.show(); + _refreshKey.currentState?.show(); _addedStudentFlag = true; } }, trailing: FutureBuilder( - future: ParentTheme.of(context).getColorsForStudent(students[index].id), + future: ParentTheme.of(context)?.getColorsForStudent(students[index].id), builder: (context, snapshot) { var color = snapshot.hasData - ? ParentTheme.of(context).getColorVariantForCurrentState(snapshot.data) + ? ParentTheme.of(context)?.getColorVariantForCurrentState(snapshot.data!) ?? Colors.transparent : Colors.transparent; return Semantics( container: true, - label: L10n(context).changeStudentColorLabel(students[index].shortName), + label: L10n(context).changeStudentColorLabel(students[index].shortName ?? ''), child: InkResponse( highlightColor: color, onTap: () { @@ -184,7 +184,7 @@ class _ManageStudentsState extends State { locator().pairNewStudent( context, () { - _refreshKey.currentState.show(); + _refreshKey.currentState?.show(); _addedStudentFlag = true; }, ); @@ -197,6 +197,6 @@ class _ManageStudentsState extends State { setState(() { _studentsFuture = _loadStudents(); }); - return _studentsFuture.catchError((_) {}); + return _studentsFuture!.catchError((_) {}); } } diff --git a/apps/flutter_parent/lib/screens/manage_students/student_color_picker_dialog.dart b/apps/flutter_parent/lib/screens/manage_students/student_color_picker_dialog.dart index 2f89b61877..5d0497c48d 100644 --- a/apps/flutter_parent/lib/screens/manage_students/student_color_picker_dialog.dart +++ b/apps/flutter_parent/lib/screens/manage_students/student_color_picker_dialog.dart @@ -25,13 +25,13 @@ class StudentColorPickerDialog extends StatefulWidget { final String studentId; final Color initialColor; - const StudentColorPickerDialog({Key key, @required this.initialColor, @required this.studentId}) : super(key: key); + const StudentColorPickerDialog({required this.initialColor, required this.studentId, super.key}); @override _StudentColorPickerDialogState createState() => _StudentColorPickerDialogState(); } class _StudentColorPickerDialogState extends State { - Color _selectedColor; + late Color _selectedColor; bool _saving = false; bool _error = false; @@ -62,7 +62,7 @@ class _StudentColorPickerDialogState extends State { ), actions: [ if (_saving) - FlatButton( + TextButton( child: Container( width: 18, height: 18, @@ -70,8 +70,8 @@ class _StudentColorPickerDialogState extends State { ), onPressed: null, ), - if (!_saving) FlatButton(child: Text(L10n(context).cancel), onPressed: () => Navigator.of(context).pop(false)), - if (!_saving) FlatButton(child: Text(L10n(context).ok), onPressed: _save), + if (!_saving) TextButton(child: Text(L10n(context).cancel), onPressed: () => Navigator.of(context).pop(false)), + if (!_saving) TextButton(child: Text(L10n(context).ok), onPressed: _save), ], ); } @@ -88,7 +88,7 @@ class _StudentColorPickerDialogState extends State { Widget _colorOption(StudentColorSet colorSet) { var selected = _selectedColor == colorSet.light; - var displayColor = ParentTheme.of(context).getColorVariantForCurrentState(colorSet); + var displayColor = ParentTheme.of(context)?.getColorVariantForCurrentState(colorSet) ?? Colors.transparent; return Semantics( selected: selected, label: StudentColorSet.getA11yName(colorSet, context), @@ -128,7 +128,7 @@ class _StudentColorPickerDialogState extends State { try { await locator().save(widget.studentId, _selectedColor); - ParentTheme.of(context).refreshStudentColor(); + ParentTheme.of(context)?.refreshStudentColor(); Navigator.of(context).pop(true); } catch (e, s) { setState(() { diff --git a/apps/flutter_parent/lib/screens/manage_students/student_color_picker_interactor.dart b/apps/flutter_parent/lib/screens/manage_students/student_color_picker_interactor.dart index 0fe5d12605..1112c4cec2 100644 --- a/apps/flutter_parent/lib/screens/manage_students/student_color_picker_interactor.dart +++ b/apps/flutter_parent/lib/screens/manage_students/student_color_picker_interactor.dart @@ -23,9 +23,9 @@ class StudentColorPickerInteractor { Future save(String studentId, Color newColor) async { var contextId = 'user_$studentId'; final userColorsResponse = await locator().setUserColor(contextId, newColor); - if (userColorsResponse.hexCode != null) { + if (userColorsResponse?.hexCode != null) { UserColor data = UserColor((b) => b - ..userId = ApiPrefs.getUser().id + ..userId = ApiPrefs.getUser()?.id ..userDomain = ApiPrefs.getDomain() ..canvasContext = contextId ..color = newColor); diff --git a/apps/flutter_parent/lib/screens/masquerade/masquerade_screen.dart b/apps/flutter_parent/lib/screens/masquerade/masquerade_screen.dart index c03cb0d51c..d8d8159ffb 100644 --- a/apps/flutter_parent/lib/screens/masquerade/masquerade_screen.dart +++ b/apps/flutter_parent/lib/screens/masquerade/masquerade_screen.dart @@ -14,7 +14,6 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/utils/common_widgets/respawn.dart'; @@ -33,13 +32,13 @@ class MasqueradeScreen extends StatefulWidget { class MasqueradeScreenState extends State { static Offset pandaMaskOffset = const Offset(150, 40); - TextEditingController _domainController; - TextEditingController _userIdController; + late TextEditingController _domainController; + late TextEditingController _userIdController; MasqueradeScreenInteractor _interactor = locator(); - bool _enableDomainInput; + late bool _enableDomainInput; - String _domainErrorText; - String _userIdErrorText; + String? _domainErrorText; + String? _userIdErrorText; final GlobalKey _scaffoldKey = GlobalKey(); @@ -51,11 +50,11 @@ class MasqueradeScreenState extends State { bool _startingMasquerade = false; - Timer timer; + late Timer timer; @override void initState() { - _enableDomainInput = _interactor.getDomain().contains(MasqueradeScreenInteractor.siteAdminDomain); + _enableDomainInput = _interactor.getDomain()?.contains(MasqueradeScreenInteractor.siteAdminDomain) ?? false; // Set up Domain input controller _domainController = TextEditingController(text: _enableDomainInput ? null : _interactor.getDomain()); @@ -99,7 +98,7 @@ class MasqueradeScreenState extends State { key: _scaffoldKey, appBar: AppBar( title: Text(L10n(context).actAsUser), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: SingleChildScrollView( child: Padding( @@ -152,9 +151,12 @@ class MasqueradeScreenState extends State { Container( width: double.maxFinite, height: 64, - child: RaisedButton( - textColor: Colors.white, - child: Text(L10n(context).actAsUser), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + textStyle: TextStyle(color: Colors.white), + ), + child: Text(L10n(context).actAsUser, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white),), onPressed: () => _startMasquerading(), ), ) @@ -172,7 +174,7 @@ class MasqueradeScreenState extends State { width: double.maxFinite, height: 146, child: Stack( - overflow: Overflow.visible, + clipBehavior: Clip.none, children: [ Positioned(child: SvgPicture.asset('assets/svg/masquerade-white-panda.svg'), left: 0, right: 0), AnimatedPositioned( @@ -217,10 +219,10 @@ class MasqueradeScreenState extends State { setState(() => _startingMasquerade = true); bool success = await _interactor.startMasquerading(userId, domain); if (success) { - Respawn.of(context).restart(); + Respawn.of(context)?.restart(); } else { setState(() => _startingMasquerade = false); - _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(L10n(context).actAsUserError))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(L10n(context).actAsUserError))); } } } diff --git a/apps/flutter_parent/lib/screens/masquerade/masquerade_screen_interactor.dart b/apps/flutter_parent/lib/screens/masquerade/masquerade_screen_interactor.dart index d206455b49..ac6f0f1144 100644 --- a/apps/flutter_parent/lib/screens/masquerade/masquerade_screen_interactor.dart +++ b/apps/flutter_parent/lib/screens/masquerade/masquerade_screen_interactor.dart @@ -17,7 +17,7 @@ import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class MasqueradeScreenInteractor { - String getDomain() => ApiPrefs.getDomain(); + String? getDomain() => ApiPrefs.getDomain(); static final siteAdminDomain = 'siteadmin.instructure.com'; @@ -26,7 +26,7 @@ class MasqueradeScreenInteractor { var user = await locator().getUserForDomain(masqueradingDomain, masqueradingUserId); ApiPrefs.updateCurrentLogin((b) => b ..masqueradeDomain = masqueradingDomain - ..masqueradeUser = user.toBuilder()); + ..masqueradeUser = user?.toBuilder()); return true; } catch (e) { return false; @@ -35,7 +35,7 @@ class MasqueradeScreenInteractor { /// Cleans up the input domain and adds protocol and '.instructure.com' as necessary. Returns an empty string /// if sanitizing failed. - String sanitizeDomain(String domain) { + String sanitizeDomain(String? domain) { if (domain == null || domain.isEmpty) return ''; // Remove white space diff --git a/apps/flutter_parent/lib/screens/not_a_parent_screen.dart b/apps/flutter_parent/lib/screens/not_a_parent_screen.dart index 1f3f9c9826..f5ed355cf2 100644 --- a/apps/flutter_parent/lib/screens/not_a_parent_screen.dart +++ b/apps/flutter_parent/lib/screens/not_a_parent_screen.dart @@ -61,14 +61,14 @@ class NotAParentScreen extends StatelessWidget { child: Text( L10n(context).studentOrTeacherTitle, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.bodySmall, ), ), children: [ Text( L10n(context).studentOrTeacherSubtitle, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption, + style: Theme.of(context).textTheme.bodySmall, ), SizedBox(height: 24), _appButton(context, L10n(context).studentApp, L10n(context).canvasStudentApp, ParentColors.studentApp, () { diff --git a/apps/flutter_parent/lib/screens/pairing/pairing_code_dialog.dart b/apps/flutter_parent/lib/screens/pairing/pairing_code_dialog.dart index 7d49a79a04..ba2630c49f 100644 --- a/apps/flutter_parent/lib/screens/pairing/pairing_code_dialog.dart +++ b/apps/flutter_parent/lib/screens/pairing/pairing_code_dialog.dart @@ -23,9 +23,9 @@ import 'package:flutter_parent/utils/service_locator.dart'; class PairingCodeDialog extends StatefulWidget { final _interactor = locator(); - final String _pairingCode; + final String? _pairingCode; - PairingCodeDialog(this._pairingCode, {Key key}); + PairingCodeDialog(this._pairingCode, {super.key}); @override State createState() => PairingCodeDialogState(); @@ -60,14 +60,14 @@ class PairingCodeDialogState extends State { padding: const EdgeInsets.only(bottom: 20.0), child: Text( L10n(context).pairingCodeEntryExplanation, - style: Theme.of(context).textTheme.bodyText2.copyWith(fontSize: 12.0), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 12.0), ), ), TextFormField( key: _formKey, autofocus: true, autocorrect: false, - autovalidate: false, + autovalidateMode: AutovalidateMode.disabled, initialValue: widget._pairingCode, onChanged: (value) { _showPairingCodeError(false); @@ -102,7 +102,7 @@ class PairingCodeDialogState extends State { }); }, onFieldSubmitted: (code) async { - _formKey.currentState.save(); + _formKey.currentState?.save(); }, decoration: InputDecoration( hintText: L10n(context).pairingCode, @@ -115,9 +115,9 @@ class PairingCodeDialogState extends State { ), ), actions: [ - FlatButton( - disabledTextColor: ParentColors.parentApp.withAlpha(100), + TextButton( child: Text(L10n(context).cancel.toUpperCase()), + style: TextButton.styleFrom(disabledForegroundColor: Theme.of(context).primaryColor.withAlpha(100)), onPressed: _makingApiCall ? null : () { @@ -125,14 +125,14 @@ class PairingCodeDialogState extends State { Navigator.of(context).pop(false); }, ), - FlatButton( - disabledTextColor: ParentColors.parentApp.withAlpha(100), + TextButton( + style: TextButton.styleFrom(disabledForegroundColor: Theme.of(context).primaryColor.withAlpha(100)), child: Text(L10n(context).ok), onPressed: _makingApiCall ? null : () async { _showPairingCodeError(false); - _formKey.currentState.save(); + _formKey.currentState?.save(); }, ), ], @@ -141,7 +141,7 @@ class PairingCodeDialogState extends State { } void _showPairingCodeError(bool show) { - _formKey.currentState.validate(); + _formKey.currentState?.validate(); // Update the UI with the error state setState(() { _pairingCodeError = show; diff --git a/apps/flutter_parent/lib/screens/pairing/pairing_interactor.dart b/apps/flutter_parent/lib/screens/pairing/pairing_interactor.dart index a8655ad947..5027f8ac55 100644 --- a/apps/flutter_parent/lib/screens/pairing/pairing_interactor.dart +++ b/apps/flutter_parent/lib/screens/pairing/pairing_interactor.dart @@ -19,5 +19,8 @@ import 'package:flutter_parent/utils/service_locator.dart'; class PairingInteractor { Future scanQRCode() => QRUtils.scanPairingCode(); - Future pairWithStudent(String pairingCode) => locator().pairWithStudent(pairingCode); + Future pairWithStudent(String? pairingCode) { + if (pairingCode == null) return Future.value(null); + return locator().pairWithStudent(pairingCode); + } } diff --git a/apps/flutter_parent/lib/screens/pairing/pairing_util.dart b/apps/flutter_parent/lib/screens/pairing/pairing_util.dart index 2cdc1448ce..73a956d6e1 100644 --- a/apps/flutter_parent/lib/screens/pairing/pairing_util.dart +++ b/apps/flutter_parent/lib/screens/pairing/pairing_util.dart @@ -27,6 +27,7 @@ import 'package:flutter_svg/flutter_svg.dart'; class PairingUtil { pairNewStudent(BuildContext context, Function() onSuccess) { showModalBottomSheet( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, context: context, isScrollControlled: true, builder: (context) { @@ -34,7 +35,7 @@ class PairingUtil { padding: EdgeInsets.symmetric(vertical: 20, horizontal: 16), shrinkWrap: true, children: [ - Text(L10n(context).addStudentWith, style: Theme.of(context).textTheme.caption), + Text(L10n(context).addStudentWith, style: Theme.of(context).textTheme.bodyMedium), SizedBox(height: 12), _pairingCode(context, onSuccess), if (_hasCameras()) _qrCode(context, onSuccess), @@ -46,10 +47,10 @@ class PairingUtil { Widget _qrCode(BuildContext context, Function() onSuccess) { return ListTile( - title: Text(L10n(context).qrCode), + title: Text(L10n(context).qrCode, style: Theme.of(context).textTheme.titleMedium), subtitle: Padding( padding: const EdgeInsets.only(top: 4), - child: Text(L10n(context).qrCodeDescription), + child: Text(L10n(context).qrCodeDescription, style: Theme.of(context).textTheme.bodyMedium), ), leading: SvgPicture.asset('assets/svg/qr-code.svg', color: ParentColors.ash, width: 25, height: 25), contentPadding: EdgeInsets.symmetric(vertical: 10), @@ -64,17 +65,17 @@ class PairingUtil { Widget _pairingCode(BuildContext context, Function() onSuccess) { return ListTile( - title: Text(L10n(context).pairingCode), + title: Text(L10n(context).pairingCode, style: Theme.of(context).textTheme.titleMedium), subtitle: Padding( padding: const EdgeInsets.only(top: 4), - child: Text(L10n(context).pairingCodeDescription), + child: Text(L10n(context).pairingCodeDescription, style: Theme.of(context).textTheme.bodyMedium), ), leading: Icon(CanvasIcons.keyboard_shortcuts, color: ParentColors.ash), contentPadding: EdgeInsets.symmetric(vertical: 10), onTap: () async { Navigator.of(context).pop(); locator().logEvent(AnalyticsEventConstants.ADD_STUDENT_MANAGE_STUDENTS); - bool studentPaired = await locator().showDialog( + bool? studentPaired = await locator().showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) => PairingCodeDialog(null), diff --git a/apps/flutter_parent/lib/screens/pairing/qr_pairing_screen.dart b/apps/flutter_parent/lib/screens/pairing/qr_pairing_screen.dart index ece80b406b..b0d7a42fa5 100644 --- a/apps/flutter_parent/lib/screens/pairing/qr_pairing_screen.dart +++ b/apps/flutter_parent/lib/screens/pairing/qr_pairing_screen.dart @@ -28,10 +28,10 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:tuple/tuple.dart'; class QRPairingScreen extends StatefulWidget { - final QRPairingInfo pairingInfo; + final QRPairingInfo? pairingInfo; final bool isCreatingAccount; - const QRPairingScreen({Key key, this.pairingInfo, this.isCreatingAccount = false}) : super(key: key); + const QRPairingScreen({this.pairingInfo, this.isCreatingAccount = false, super.key}); @override _QRPairingScreenState createState() => _QRPairingScreenState(); @@ -41,7 +41,7 @@ class _QRPairingScreenState extends State { final GlobalKey _scaffoldKey = GlobalKey(); bool _isPairing = false; - Tuple2 _errorInfo; + Tuple2? _errorInfo; PairingInteractor _interactor = locator(); @@ -50,7 +50,7 @@ class _QRPairingScreenState extends State { if (widget.pairingInfo != null) { _isPairing = true; WidgetsBinding.instance.addPostFrameCallback((_) { - _handleScanResult(widget.pairingInfo); + _handleScanResult(widget.pairingInfo!); }); } super.initState(); @@ -65,7 +65,7 @@ class _QRPairingScreenState extends State { appBar: AppBar( title: Text(showTutorial ? L10n(context).qrPairingTutorialTitle : L10n(context).qrPairingTitle), automaticallyImplyLeading: true, - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), actions: [ if (showTutorial) _nextButton(context), ], @@ -90,7 +90,7 @@ class _QRPairingScreenState extends State { padding: const EdgeInsets.all(16), child: Column( children: [ - Text(L10n(context).qrPairingTutorialMessage, style: Theme.of(context).textTheme.subtitle1), + Text(L10n(context).qrPairingTutorialMessage, style: Theme.of(context).textTheme.titleMedium), Expanded( child: FractionallySizedBox( alignment: Alignment.center, @@ -110,10 +110,12 @@ class _QRPairingScreenState extends State { return ButtonTheme( textTheme: Theme.of(context).buttonTheme.textTheme, minWidth: 48, - child: FlatButton( - visualDensity: VisualDensity.compact, + child: TextButton( + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + shape: CircleBorder(side: BorderSide(color: Colors.transparent)), + ), child: Text(L10n(context).next.toUpperCase()), - shape: CircleBorder(side: BorderSide(color: Colors.transparent)), onPressed: () async { QRPairingScanResult result = await _interactor.scanQRCode(); _handleScanResult(result); @@ -125,8 +127,8 @@ class _QRPairingScreenState extends State { Widget _errorMessage(BuildContext context) { return EmptyPandaWidget( svgPath: 'assets/svg/panda-no-pairing-code.svg', - title: _errorInfo.item1, - subtitle: _errorInfo.item2, + title: _errorInfo!.item1, + subtitle: _errorInfo!.item2, buttonText: L10n(context).retry, onButtonTap: () async { QRPairingScanResult result = await _interactor.scanQRCode(); @@ -153,7 +155,7 @@ class _QRPairingScreenState extends State { } } else if (result is QRPairingScanError) { locator().logMessage(result.type.toString()); - Tuple2 errorInfo; + Tuple2? errorInfo; switch (result.type) { case QRPairingScanErrorType.invalidCode: errorInfo = Tuple2(l10n.qrPairingInvalidCodeTitle, l10n.invalidQRCodeError); @@ -178,11 +180,11 @@ class _QRPairingScreenState extends State { void _handleScanResultForPairing(QRPairingInfo result) async { var l10n = L10n(context); // 'success' is true if pairing worked, false for API/pairing error, null for network error - bool success = await _interactor.pairWithStudent(result.code); + bool? success = await _interactor.pairWithStudent(result.code); if (success == true) { // If opened from a deep link in a cold state, this will be the top route and we'll want to go to the splash instead of popping - if (ModalRoute.of(context).isFirst) { + if (ModalRoute.of(context)?.isFirst == true) { locator().replaceRoute(context, PandaRouter.rootSplash()); } else { locator().notify(); @@ -191,7 +193,7 @@ class _QRPairingScreenState extends State { } else { Tuple2 errorInfo; if (success == false) { - if (ApiPrefs.isLoggedIn() && !ApiPrefs.getDomain().endsWith(result.domain)) { + if (ApiPrefs.isLoggedIn() && ApiPrefs.getDomain()?.endsWith(result.domain) == false) { errorInfo = Tuple2(l10n.qrPairingWrongDomainTitle, l10n.qrPairingWrongDomainSubtitle); } else { errorInfo = Tuple2(l10n.qrPairingFailedTitle, l10n.qrPairingFailedSubtitle); diff --git a/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen.dart b/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen.dart index 2cf218a84e..1de7c14423 100644 --- a/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen.dart +++ b/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen.dart @@ -40,7 +40,7 @@ class _QRLoginTutorialScreenState extends State { title: Text(L10n(context).locateQRCode), automaticallyImplyLeading: true, actions: [_nextButton(context)], - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: _body(context), )); @@ -50,25 +50,25 @@ class _QRLoginTutorialScreenState extends State { return MaterialButton( minWidth: 20, highlightColor: Colors.transparent, - splashColor: Theme.of(context).accentColor.withAlpha(100), - textColor: Theme.of(context).accentColor, + splashColor: Theme.of(context).colorScheme.secondary.withAlpha(100), + textColor: Theme.of(context).colorScheme.secondary, shape: CircleBorder(side: BorderSide(color: Colors.transparent)), onPressed: () async { var barcodeResult = await locator().scan(); - if (barcodeResult.isSuccess) { - final result = await locator().pushRoute(context, PandaRouter.qrLogin(barcodeResult.result)); + if (barcodeResult.isSuccess && barcodeResult.result != null) { + final result = await locator().pushRoute(context, PandaRouter.qrLogin(barcodeResult.result!)); // Await this result so we can show an error message if the splash screen has to pop after a login issue // (This is typically in the case of the same QR code being scanned twice) - if (result != null) { + if (result != null && result is String) { _showSnackBarError(context, result); } } else if (barcodeResult.errorType == QRError.invalidQR || barcodeResult.errorType == QRError.cameraError) { // We only want to display an error for invalid and camera denied, the other case is the user cancelled - locator().logMessage(barcodeResult?.errorType?.toString() ?? 'No barcode result'); + locator().logMessage(barcodeResult.errorType.toString()); _showSnackBarError( context, - barcodeResult?.errorType == QRError.invalidQR + barcodeResult.errorType == QRError.invalidQR ? L10n(context).invalidQRCodeError : L10n(context).qrCodeNoCameraError); } @@ -108,7 +108,7 @@ class _QRLoginTutorialScreenState extends State { } _showSnackBarError(BuildContext context, String error) { - _scaffoldKey.currentState.removeCurrentSnackBar(); - _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(error))); + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); } } diff --git a/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen_interactor.dart b/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen_interactor.dart index d622b5b2c8..d236da76c3 100644 --- a/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen_interactor.dart +++ b/apps/flutter_parent/lib/screens/qr_login/qr_login_tutorial_screen_interactor.dart @@ -60,8 +60,8 @@ class QRLoginTutorialScreenInteractor { class BarcodeScanResult { final bool isSuccess; - final QRError errorType; - final String result; + final QRError? errorType; + final String? result; BarcodeScanResult(this.isSuccess, {this.errorType = null, this.result = null}); } diff --git a/apps/flutter_parent/lib/screens/qr_login/qr_login_util.dart b/apps/flutter_parent/lib/screens/qr_login/qr_login_util.dart index e33ddea76e..7d36404e08 100644 --- a/apps/flutter_parent/lib/screens/qr_login/qr_login_util.dart +++ b/apps/flutter_parent/lib/screens/qr_login/qr_login_util.dart @@ -29,7 +29,7 @@ class QRLoginUtil { padding: EdgeInsets.symmetric(vertical: 20, horizontal: 16), shrinkWrap: true, children: [ - Text(L10n(context).qrLoginSelect, style: Theme.of(context).textTheme.caption), + Text(L10n(context).qrLoginSelect, style: Theme.of(context).textTheme.bodySmall), SizedBox(height: 12), _login(context), _createAccount(context), diff --git a/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart b/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart index a0e0dc4858..6100c40408 100644 --- a/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart +++ b/apps/flutter_parent/lib/screens/remote_config/remote_config_screen.dart @@ -25,7 +25,7 @@ class RemoteConfigScreen extends StatefulWidget { class _RemoteConfigScreenState extends State { RemoteConfigInteractor _interactor = locator(); - Map _remoteConfig; + late Map _remoteConfig; @override void initState() { diff --git a/apps/flutter_parent/lib/screens/help/legal_screen.dart b/apps/flutter_parent/lib/screens/settings/legal_screen.dart similarity index 88% rename from apps/flutter_parent/lib/screens/help/legal_screen.dart rename to apps/flutter_parent/lib/screens/settings/legal_screen.dart index 4805cfba86..c6b56ff3cd 100644 --- a/apps/flutter_parent/lib/screens/help/legal_screen.dart +++ b/apps/flutter_parent/lib/screens/settings/legal_screen.dart @@ -28,7 +28,7 @@ class LegalScreen extends StatelessWidget { builder: (context) => Scaffold( appBar: AppBar( title: Text(l10n.helpLegalLabel), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: ListView( children: [ @@ -59,7 +59,7 @@ class _LegalRow extends StatelessWidget { final VoidCallback onTap; final IconData icon; - const _LegalRow({Key key, this.label, this.onTap, this.icon}) : super(key: key); + const _LegalRow({ required this.label, required this.onTap, required this.icon, super.key}); @override Widget build(BuildContext context) { @@ -68,9 +68,9 @@ class _LegalRow extends StatelessWidget { return ListTile( title: Row( children: [ - Icon(icon, color: Theme.of(context).accentColor, size: 20), + Icon(icon, color: Theme.of(context).colorScheme.secondary, size: 20), SizedBox(width: 20), - Expanded(child: Text(label, style: textTheme.subtitle1)), + Expanded(child: Text(label, style: textTheme.titleMedium)), ], ), onTap: onTap, diff --git a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart index 652a26fc44..8f92fb9995 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart @@ -13,7 +13,6 @@ // along with this program. If not, see . import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; @@ -24,8 +23,9 @@ import 'package:flutter_parent/utils/design/theme_transition/theme_transition_ta import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import '../../router/panda_router.dart'; import '../theme_viewer_screen.dart'; class SettingsInteractor { @@ -39,8 +39,12 @@ class SettingsInteractor { locator().push(context, RemoteConfigScreen()); } - void toggleDarkMode(context, anchorKey) { - if (ParentTheme.of(context).isDarkMode) { + void routeToLegal(BuildContext context) { + locator().pushRoute(context, PandaRouter.legal()); + } + + void toggleDarkMode(BuildContext context, GlobalKey>? anchorKey) { + if (ParentTheme.of(context)?.isDarkMode == true) { locator().logEvent(AnalyticsEventConstants.DARK_MODE_OFF); } else { locator().logEvent(AnalyticsEventConstants.DARK_MODE_ON); @@ -49,12 +53,12 @@ class SettingsInteractor { } void toggleHCMode(context) { - if (ParentTheme.of(context).isHC) { + if (ParentTheme.of(context)?.isHC == true) { locator().logEvent(AnalyticsEventConstants.HC_MODE_OFF); } else { locator().logEvent(AnalyticsEventConstants.HC_MODE_ON); } - ParentTheme.of(context).toggleHC(); + ParentTheme.of(context)?.toggleHC(); } void showAboutDialog(context) { @@ -73,25 +77,25 @@ class SettingsInteractor { children: [ Text(L10n(context).aboutAppTitle, style: TextStyle(fontSize: 16)), - Text(snapshot.data.appName, style: TextStyle(fontSize: 14)), + Text(snapshot.data!.appName, style: TextStyle(fontSize: 14)), SizedBox(height: 24), Text(L10n(context).aboutDomainTitle, style: TextStyle(fontSize: 16)), - Text(ApiPrefs.getDomain(), style: TextStyle(fontSize: 14)), + Text(ApiPrefs.getDomain() ?? '', style: TextStyle(fontSize: 14)), SizedBox(height: 24), Text(L10n(context).aboutLoginIdTitle, style: TextStyle(fontSize: 16)), - Text(ApiPrefs.getUser().loginId, + Text(ApiPrefs.getUser()?.loginId ?? '', style: TextStyle(fontSize: 14)), SizedBox(height: 24), Text(L10n(context).aboutEmailTitle, style: TextStyle(fontSize: 16)), - Text(ApiPrefs.getUser().primaryEmail, + Text(ApiPrefs.getUser()?.primaryEmail ?? '', style: TextStyle(fontSize: 14)), SizedBox(height: 24), Text(L10n(context).aboutVersionTitle, style: TextStyle(fontSize: 16)), - Text(snapshot.data.version, style: TextStyle(fontSize: 14)), + Text(snapshot.data!.version, style: TextStyle(fontSize: 14)), SizedBox(height: 32), SvgPicture.asset( 'assets/svg/ic_instructure_logo.svg', diff --git a/apps/flutter_parent/lib/screens/settings/settings_screen.dart b/apps/flutter_parent/lib/screens/settings/settings_screen.dart index 9b3d4bb798..cb4ab4f9b2 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_screen.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_screen.dart @@ -12,7 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; @@ -28,8 +27,8 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - Key _lightModeKey = GlobalKey(); - Key _darkModeKey = GlobalKey(); + var _lightModeKey = GlobalKey(); + var _darkModeKey = GlobalKey(); Key _highContrastModeKey = GlobalKey(); SettingsInteractor _interactor = locator(); @@ -40,23 +39,23 @@ class _SettingsScreenState extends State { child: DefaultParentTheme( builder: (context) => Scaffold( appBar: AppBar( - title: Text(L10n(context).settings), - bottom: - ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + title: Text(L10n(context).settings, style: Theme.of(context).textTheme.titleLarge), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: ListView( children: [ Container( child: ListTile( - title: Text(L10n(context).theme), + title: Text(L10n(context).theme, style: Theme.of(context).textTheme.bodyMedium), ), ), _themeButtons(context), SizedBox(height: 16), - if (ParentTheme.of(context).isDarkMode) + if (ParentTheme.of(context)?.isDarkMode == true) _webViewDarkModeSwitch(context), _highContrastModeSwitch(context), _about(context), + _legal(context), if (_interactor.isDebugMode()) _themeViewer(context), if (_interactor.isDebugMode()) _remoteConfigs(context) ], @@ -76,7 +75,7 @@ class _SettingsScreenState extends State { anchorKey: _lightModeKey, buttonKey: Key('light-mode-button'), context: context, - selected: !ParentTheme.of(context).isDarkMode, + selected: ParentTheme.of(context)?.isDarkMode == false, semanticsLabel: L10n(context).lightModeLabel, child: SvgPicture.asset( 'assets/svg/panda-light-mode.svg', @@ -88,7 +87,7 @@ class _SettingsScreenState extends State { anchorKey: _darkModeKey, buttonKey: Key('dark-mode-button'), context: context, - selected: ParentTheme.of(context).isDarkMode, + selected: ParentTheme.of(context)?.isDarkMode == true, semanticsLabel: L10n(context).darkModeLabel, child: SvgPicture.asset( 'assets/svg/panda-dark-mode.svg', @@ -102,12 +101,12 @@ class _SettingsScreenState extends State { } Widget _themeOption({ - GlobalKey anchorKey, - Key buttonKey, - BuildContext context, - bool selected, - String semanticsLabel, - Widget child, + GlobalKey? anchorKey, + Key? buttonKey, + required BuildContext context, + required bool selected, + String? semanticsLabel, + required Widget child, }) { double size = 140; return Semantics( @@ -124,7 +123,7 @@ class _SettingsScreenState extends State { ? BoxDecoration( borderRadius: BorderRadius.circular(100), border: - Border.all(color: Theme.of(context).accentColor, width: 2), + Border.all(color: Theme.of(context).colorScheme.secondary, width: 2), ) : null, child: ClipRRect( @@ -151,9 +150,9 @@ class _SettingsScreenState extends State { Widget _webViewDarkModeSwitch(BuildContext context) { return MergeSemantics( child: ListTile( - title: Text(L10n(context).webViewDarkModeLabel), + title: Text(L10n(context).webViewDarkModeLabel, style: Theme.of(context).textTheme.bodyMedium), trailing: Switch( - value: ParentTheme.of(context).isWebViewDarkMode, + value: ParentTheme.of(context)?.isWebViewDarkMode == true, onChanged: (_) => _toggleWebViewDarkMode(context), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -163,21 +162,21 @@ class _SettingsScreenState extends State { } _toggleWebViewDarkMode(BuildContext context) { - if (ParentTheme.of(context).isWebViewDarkMode) { + if (ParentTheme.of(context)?.isWebViewDarkMode == true) { locator().logEvent(AnalyticsEventConstants.DARK_WEB_MODE_OFF); } else { locator().logEvent(AnalyticsEventConstants.DARK_WEB_MODE_ON); } - ParentTheme.of(context).toggleWebViewDarkMode(); + ParentTheme.of(context)?.toggleWebViewDarkMode(); } Widget _highContrastModeSwitch(BuildContext context) { return MergeSemantics( child: ListTile( - title: Text(L10n(context).highContrastLabel), + title: Text(L10n(context).highContrastLabel, style: Theme.of(context).textTheme.bodyMedium), trailing: Switch( key: _highContrastModeKey, - value: ParentTheme.of(context).isHC, + value: ParentTheme.of(context)?.isHC == true, onChanged: (_) => _onHighContrastModeChanged(context), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -193,17 +192,21 @@ class _SettingsScreenState extends State { Widget _about(BuildContext context) => ListTile( key: Key('about'), title: Row( - children: [Text(L10n(context).about)], + children: [Text(L10n(context).about, style: Theme.of(context).textTheme.bodyMedium)], ), onTap: () => _interactor.showAboutDialog(context)); + Widget _legal(BuildContext context) => ListTile( + title: Text(L10n(context).helpLegalLabel, style: Theme.of(context).textTheme.bodyMedium), + onTap: () => _interactor.routeToLegal(context)); + Widget _themeViewer(BuildContext context) => ListTile( key: Key('theme-viewer'), title: Row( children: [ _debugLabel(context), SizedBox(width: 16), - Text('Theme Viewer'), // Not shown in release mode, not translated + Text('Theme Viewer', style: Theme.of(context).textTheme.bodyMedium), // Not shown in release mode, not translated ], ), onTap: () => _interactor.routeToThemeViewer(context), @@ -215,7 +218,7 @@ class _SettingsScreenState extends State { children: [ _debugLabel(context), SizedBox(width: 16), - Text('Remote Config Params') + Text('Remote Config Params', style: Theme.of(context).textTheme.bodyMedium) ], ), onTap: () => _interactor.routeToRemoteConfig(context), @@ -224,12 +227,12 @@ class _SettingsScreenState extends State { Container _debugLabel(BuildContext context) { return Container( decoration: BoxDecoration( - color: Theme.of(context).accentColor, + color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(32), ), padding: const EdgeInsets.all(4), child: Icon(Icons.bug_report, - color: Theme.of(context).accentIconTheme.color, size: 16), + color: Theme.of(context).colorScheme.secondary, size: 16), ); } } diff --git a/apps/flutter_parent/lib/screens/splash/splash_screen.dart b/apps/flutter_parent/lib/screens/splash/splash_screen.dart index 154a388876..d682b3ab48 100644 --- a/apps/flutter_parent/lib/screens/splash/splash_screen.dart +++ b/apps/flutter_parent/lib/screens/splash/splash_screen.dart @@ -22,27 +22,26 @@ import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/splash/splash_screen_interactor.dart'; import 'package:flutter_parent/utils/common_widgets/canvas_loading_indicator.dart'; import 'package:flutter_parent/utils/common_widgets/masquerade_ui.dart'; -import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class SplashScreen extends StatefulWidget { - final String qrLoginUrl; + final String? qrLoginUrl; - SplashScreen({this.qrLoginUrl, Key key}) : super(key: key); + SplashScreen({this.qrLoginUrl, super.key}); @override _SplashScreenState createState() => _SplashScreenState(); } class _SplashScreenState extends State with SingleTickerProviderStateMixin { - Future _dataFuture; - Future _cameraFuture; + Future? _dataFuture; + Future? _cameraFuture; // Controller and animation used on the loading indicator for the 'zoom out' effect immediately before routing - AnimationController _controller; - Animation _animation; - String _route; + late AnimationController _controller; + late Animation _animation; + late String _route; @override void initState() { @@ -79,9 +78,9 @@ class _SplashScreenState extends State with SingleTickerProviderSt backgroundColor: Theme.of(context).primaryColor, body: FutureBuilder( future: _dataFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { - if (snapshot.data.isObserver || snapshot.data.canMasquerade) { + if (snapshot.data!.isObserver || snapshot.data!.canMasquerade) { _navigateToDashboardOrAup(); } else { // User is not an observer and cannot masquerade. Show the not-a-parent screen. @@ -130,7 +129,7 @@ class _SplashScreenState extends State with SingleTickerProviderSt locator() .isTermsAcceptanceRequired() .then((aupRequired) => { - if (aupRequired) { + if (aupRequired == true) { _navigate(PandaRouter.aup()) } else { @@ -186,15 +185,14 @@ class _SplashScreenState extends State with SingleTickerProviderSt class _CircleClipTransition extends AnimatedWidget { const _CircleClipTransition({ - Key key, - @required Animation scale, + required Animation scale, this.child, - }) : assert(scale != null), - super(key: key, listenable: scale); + super.key + }) : super(listenable: scale); - Animation get animation => listenable; + Animation get animation => listenable as Animation; - final Widget child; + final Widget? child; @override Widget build(BuildContext context) { diff --git a/apps/flutter_parent/lib/screens/splash/splash_screen_interactor.dart b/apps/flutter_parent/lib/screens/splash/splash_screen_interactor.dart index 815eab769d..906278f7d5 100644 --- a/apps/flutter_parent/lib/screens/splash/splash_screen_interactor.dart +++ b/apps/flutter_parent/lib/screens/splash/splash_screen_interactor.dart @@ -31,11 +31,11 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/veneers/barcode_scan_veneer.dart'; class SplashScreenInteractor { - Future getData({String qrLoginUrl}) async { + Future getData({String? qrLoginUrl}) async { if (qrLoginUrl != null) { // Double check the loginUrl final qrLoginUri = QRUtils.verifySSOLogin(qrLoginUrl); - if (qrLoginUrl == null) { + if (qrLoginUri == null) { locator().logEvent(AnalyticsEventConstants.QR_LOGIN_FAILURE); return Future.error(QRLoginError()); } else { @@ -58,23 +58,23 @@ class SplashScreenInteractor { // Use same call as the dashboard so results will be cached var students = await locator().getStudents(forceRefresh: true); - var isObserver = students.isNotEmpty; + var isObserver = students?.isNotEmpty ?? false; // Check for masquerade permissions if we haven't already - if (ApiPrefs.getCurrentLogin().canMasquerade == null) { - if (ApiPrefs.getDomain().contains(MasqueradeScreenInteractor.siteAdminDomain)) { + if (ApiPrefs.getCurrentLogin()?.canMasquerade == null) { + if (ApiPrefs.getDomain()?.contains(MasqueradeScreenInteractor.siteAdminDomain) == true) { ApiPrefs.updateCurrentLogin((b) => b..canMasquerade = true); } else { try { var permissions = await locator().getAccountPermissions(); - ApiPrefs.updateCurrentLogin((b) => b..canMasquerade = permissions.becomeUser); + ApiPrefs.updateCurrentLogin((b) => b..canMasquerade = permissions?.becomeUser); } catch (e) { ApiPrefs.updateCurrentLogin((b) => b..canMasquerade = false); } } } - SplashScreenData data = SplashScreenData(isObserver, ApiPrefs.getCurrentLogin().canMasquerade); + SplashScreenData data = SplashScreenData(isObserver, ApiPrefs.getCurrentLogin()?.canMasquerade == true); if (data.isObserver || data.canMasquerade) await updateUserColors(); @@ -85,7 +85,8 @@ class SplashScreenInteractor { Future updateUserColors() async { var colors = await locator().getUserColors(refresh: true); - await locator().insertOrUpdateAll(ApiPrefs.getDomain(), ApiPrefs.getUser().id, colors); + if (colors == null) return; + await locator().insertOrUpdateAll(ApiPrefs.getDomain(), ApiPrefs.getUser()?.id, colors); } Future getCameraCount() async { @@ -94,21 +95,21 @@ class SplashScreenInteractor { await ApiPrefs.setCameraCount(cameraCount); return cameraCount; } else { - return ApiPrefs.getCameraCount(); + return ApiPrefs.getCameraCount() ?? 0; } } Future _performSSOLogin(Uri qrLoginUri) async { - final domain = qrLoginUri.queryParameters[QRUtils.QR_DOMAIN]; - final oAuthCode = qrLoginUri.queryParameters[QRUtils.QR_AUTH_CODE]; + final domain = qrLoginUri.queryParameters[QRUtils.QR_DOMAIN] ?? ''; + final oAuthCode = qrLoginUri.queryParameters[QRUtils.QR_AUTH_CODE] ?? ''; final mobileVerifyResult = await locator().mobileVerify(domain); - if (mobileVerifyResult.result != VerifyResultEnum.success) { + if (mobileVerifyResult?.result != VerifyResultEnum.success) { return Future.value(false); } - CanvasToken tokenResponse; + CanvasToken? tokenResponse; try { tokenResponse = await locator().getTokens(mobileVerifyResult, oAuthCode); } catch (e) { @@ -116,18 +117,18 @@ class SplashScreenInteractor { } // Key here is that realUser represents a masquerading attempt - var isMasquerading = tokenResponse.realUser != null; + var isMasquerading = tokenResponse?.realUser != null; Login login = Login((b) => b - ..accessToken = tokenResponse.accessToken - ..refreshToken = tokenResponse.refreshToken - ..domain = mobileVerifyResult.baseUrl - ..clientId = mobileVerifyResult.clientId - ..clientSecret = mobileVerifyResult.clientSecret - ..masqueradeUser = isMasquerading ? tokenResponse.user.toBuilder() : null - ..masqueradeDomain = isMasquerading ? mobileVerifyResult.baseUrl : null + ..accessToken = tokenResponse?.accessToken + ..refreshToken = tokenResponse?.refreshToken + ..domain = mobileVerifyResult?.baseUrl + ..clientId = mobileVerifyResult?.clientId + ..clientSecret = mobileVerifyResult?.clientSecret + ..masqueradeUser = isMasquerading ? tokenResponse?.user?.toBuilder() : null + ..masqueradeDomain = isMasquerading ? mobileVerifyResult?.baseUrl : null ..isMasqueradingFromQRCode = isMasquerading ? true : null ..canMasquerade = isMasquerading ? true : null - ..user = tokenResponse.user.toBuilder()); + ..user = tokenResponse?.user?.toBuilder()); ApiPrefs.addLogin(login); ApiPrefs.switchLogins(login); @@ -136,13 +137,16 @@ class SplashScreenInteractor { return Future.value(true); } - Future _requiresTermsAcceptance(String targetUrl) async { - return (await locator.get().getAuthenticatedUrl(targetUrl))?.requiresTermsAcceptance ?? false; + Future _requiresTermsAcceptance(String targetUrl) async { + return (await locator.get().getAuthenticatedUrl(targetUrl))?.requiresTermsAcceptance; } - Future isTermsAcceptanceRequired() async { - final targetUrl = '${ApiPrefs.getCurrentLogin().domain}/users/self'; - if (targetUrl.contains(ApiPrefs.getDomain())) { + Future isTermsAcceptanceRequired() async { + final targetUrl = '${ApiPrefs.getCurrentLogin()?.domain}/users/self'; + String? domain = ApiPrefs.getDomain(); + if (domain == null) { + return false; + } else if (targetUrl.contains(domain)) { return _requiresTermsAcceptance(targetUrl); } else { return false; diff --git a/apps/flutter_parent/lib/screens/theme_viewer_screen.dart b/apps/flutter_parent/lib/screens/theme_viewer_screen.dart index 82373a232f..475a8ec268 100644 --- a/apps/flutter_parent/lib/screens/theme_viewer_screen.dart +++ b/apps/flutter_parent/lib/screens/theme_viewer_screen.dart @@ -36,20 +36,20 @@ class _ThemeViewerScreenState extends State { setState(() => _allToggle = !_allToggle); } - Map getStyles(TextTheme theme) => { - 'subtitle2 / caption': theme.subtitle2, - 'overline / subhead': theme.overline, - 'bodyText2 / body': theme.bodyText2, - 'caption / subtitle': theme.caption, - 'subtitle1 / title': theme.subtitle1, - 'headline5 / heading': theme.headline5, - 'headline4 / display': theme.headline4, - 'button / -': theme.button, - 'bodyText1 / -': theme.bodyText1, - 'headline6 / -': theme.headline6, - 'headline3 / -': theme.headline3, - 'headline2 / -': theme.headline2, - 'headline1 / -': theme.headline1, + Map getStyles(TextTheme theme) => { + 'subtitle2 / caption': theme.titleSmall, + 'overline / subhead': theme.labelSmall, + 'bodyText2 / body': theme.bodyMedium, + 'caption / subtitle': theme.bodySmall, + 'subtitle1 / title': theme.titleMedium, + 'headline5 / heading': theme.headlineSmall, + 'headline4 / display': theme.headlineMedium, + 'button / -': theme.labelLarge, + 'bodyText1 / -': theme.bodyLarge, + 'headline6 / -': theme.titleLarge, + 'headline3 / -': theme.displaySmall, + 'headline2 / -': theme.displayMedium, + 'headline1 / -': theme.displayLarge, }; @override @@ -65,18 +65,19 @@ class _ThemeViewerScreenState extends State { child: ListView( children: [ DrawerHeader( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + child: ListView( children: [ - Container( - key: ThemeViewerScreen.studentColorKey, - width: 48, - height: 48, - color: Theme.of(context).accentColor, + Align( + alignment: Alignment.centerLeft, + child: Container( + key: ThemeViewerScreen.studentColorKey, + width: 48, + height: 48, + color: Theme.of(context).colorScheme.secondary, + ) ), - Text('Theme configuration', style: Theme.of(context).textTheme.headline6), - Text('Play around with some values', style: Theme.of(context).textTheme.caption), + Text('Theme configuration', style: Theme.of(context).textTheme.titleLarge), + Text('Play around with some values', style: Theme.of(context).textTheme.bodySmall), ], ), ), @@ -92,14 +93,14 @@ class _ThemeViewerScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Theme Viewer'), - Text('View all the things', style: Theme.of(context).primaryTextTheme.caption), + Text('View all the things', style: Theme.of(context).primaryTextTheme.bodySmall), ], ), actions: [ IconButton(icon: Icon(CanvasIcons.email), onPressed: () {}), IconButton(icon: Icon(CanvasIcons.search), onPressed: () {}), ], - bottom: ParentTheme.of(context).appBarDivider( + bottom: ParentTheme.of(context)?.appBarDivider( bottom: TabBar( indicatorColor: Theme.of(context).primaryIconTheme.color, tabs: [ @@ -133,21 +134,21 @@ class _ThemeViewerScreenState extends State { Divider(height: 0.5, thickness: 0.5), BottomNavigationBar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, - unselectedItemColor: Theme.of(context).textTheme.caption.color, + unselectedItemColor: Theme.of(context).textTheme.bodySmall?.color, onTap: (value) => setState(() => selectedIdx = value), // new currentIndex: selectedIdx, // new items: [ new BottomNavigationBarItem( icon: Icon(CanvasIcons.courses), - title: Text('Courses'), + label: 'Courses', ), new BottomNavigationBarItem( icon: Icon(CanvasIcons.calendar_month), - title: Text('Calendar'), + label: 'Calendar', ), new BottomNavigationBarItem( icon: Icon(CanvasIcons.alerts), - title: Text('Alerts'), + label: 'Alerts', ), ], ), @@ -161,7 +162,7 @@ class _ThemeViewerScreenState extends State { } List _drawerContents(BuildContext context) { - var selectedColorSet = ParentTheme.of(context).studentColorSet; + var selectedColorSet = ParentTheme.of(context)!.studentColorSet; var colorIndex = StudentColorSet.all.indexOf(selectedColorSet); if (colorIndex == -1) colorIndex = 0; return [ @@ -170,7 +171,7 @@ class _ThemeViewerScreenState extends State { child: DropdownButton( hint: Text('Tralala'), value: colorIndex, - onChanged: (index) => ParentTheme.of(context).setSelectedStudent(index.toString()), + onChanged: (index) => ParentTheme.of(context)?.setSelectedStudent(index.toString()), isExpanded: true, items: StudentColorSet.all .asMap() @@ -184,7 +185,7 @@ class _ThemeViewerScreenState extends State { Container( width: 12, height: 12, - color: ParentTheme.of(context).getColorVariantForCurrentState(it.value), + color: ParentTheme.of(context)?.getColorVariantForCurrentState(it.value), ), SizedBox(width: 8), Flexible(child: Text('Student Color ${it.key + 1}')), @@ -196,25 +197,24 @@ class _ThemeViewerScreenState extends State { ), ), SwitchListTile( - title: Text('Dark Mode'), + title: Text('Dark Mode', style: Theme.of(context).textTheme.bodyMedium), subtitle: Text('Subtitle'), - value: ParentTheme.of(context).isDarkMode, - onChanged: (_) => ParentTheme.of(context).toggleDarkMode(), + value: ParentTheme.of(context)?.isDarkMode == true, + onChanged: (_) => ParentTheme.of(context)?.toggleDarkMode(), ), SwitchListTile( - title: Text('High Contrast Mode'), - value: ParentTheme.of(context).isHC, - onChanged: (_) => ParentTheme.of(context).toggleHC(), + title: Text('High Contrast Mode', style: Theme.of(context).textTheme.bodyMedium), + value: ParentTheme.of(context)?.isHC == true, + onChanged: (_) => ParentTheme.of(context)?.toggleHC(), ), ]; } Widget _content(BuildContext context) { - var swatch = ParentColors.makeSwatch(ParentTheme.of(context).studentColor); + var swatch = ParentColors.makeSwatch(ParentTheme.of(context)!.studentColor); return ListView( children: [ AppBar( - textTheme: Theme.of(context).textTheme, iconTheme: Theme.of(context).iconTheme, backgroundColor: Theme.of(context).scaffoldBackgroundColor, title: Column( @@ -222,7 +222,7 @@ class _ThemeViewerScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Inverse AppBar'), - Text('Inbox, creating/editing, etc', style: Theme.of(context).textTheme.caption), + Text('Inbox, creating/editing, etc', style: Theme.of(context).textTheme.bodySmall), ], ), actions: [ @@ -247,7 +247,7 @@ class _ThemeViewerScreenState extends State { i == 0 ? '50' : i.toString(), style: TextStyle( fontSize: 8, - color: swatch[i == 0 ? 50 : i].computeLuminance() > 0.5 ? Colors.black : Colors.white), + color: (swatch[i == 0 ? 50 : i]?.computeLuminance() ?? 0) > 0.5 ? Colors.black : Colors.white), ), ), ), @@ -263,17 +263,17 @@ class _ThemeViewerScreenState extends State { children: [ Padding( padding: const EdgeInsets.only(bottom: 4), - child: Text('Essay: The Rocky Planet', style: Theme.of(context).textTheme.headline4), + child: Text('Essay: The Rocky Planet', style: Theme.of(context).textTheme.headlineMedium), ), Row( children: [ - Text('100 pts', style: Theme.of(context).textTheme.caption), + Text('100 pts', style: Theme.of(context).textTheme.bodySmall), Padding( padding: const EdgeInsets.only(left: 12, right: 4), - child: Icon(Icons.check_circle, size: 20, color: ParentTheme.of(context).successColor), + child: Icon(Icons.check_circle, size: 20, color: ParentTheme.of(context)?.successColor), ), Text('Submitted', - style: Theme.of(context).textTheme.caption.apply(color: ParentTheme.of(context).successColor)), + style: Theme.of(context).textTheme.bodySmall?.apply(color: ParentTheme.of(context)?.successColor)), ], ), ], @@ -287,11 +287,11 @@ class _ThemeViewerScreenState extends State { Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text('Due', style: Theme.of(context).textTheme.overline), + child: Text('Due', style: Theme.of(context).textTheme.labelSmall), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text('April 1 at 11:59pm', style: Theme.of(context).textTheme.subtitle1), + child: Text('April 1 at 11:59pm', style: Theme.of(context).textTheme.titleMedium), ), Divider(), SwitchListTile( @@ -318,29 +318,32 @@ class _ThemeViewerScreenState extends State { Divider(), Padding( padding: const EdgeInsets.only(left: 16, top: 16), - child: Text('BIO 102', style: Theme.of(context).textTheme.overline), + child: Text('BIO 102', style: Theme.of(context).textTheme.labelSmall), ), ListTile( title: Text('ListTile Title'), subtitle: Text('ListTile Subtitle'), leading: Icon( Icons.assignment, - color: Theme.of(context).accentColor, + color: Theme.of(context).colorScheme.secondary, ), ), Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - FlatButton( + TextButton( child: Text('Flat button'), - textTheme: ButtonTextTheme.accent, + style: TextButton.styleFrom( + textStyle: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), onPressed: () {}, ), - RaisedButton( + ElevatedButton( child: Text('Raised Button'), - color: Theme.of(context).accentColor, - colorBrightness: Brightness.dark, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.onSecondary, + ), onPressed: () {}, ) ], @@ -349,15 +352,18 @@ class _ThemeViewerScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - FlatButton( + TextButton( child: Text('Flat button (disabled)'), - textTheme: ButtonTextTheme.accent, + style: TextButton.styleFrom( + textStyle: TextStyle(color: Theme.of(context).colorScheme.secondary), + ), onPressed: null, ), - RaisedButton( + ElevatedButton( child: Text('Raised Button (disabled)'), - color: Theme.of(context).accentColor, - colorBrightness: Brightness.dark, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.onSecondary, + ), onPressed: null, ) ], @@ -388,15 +394,15 @@ class _ThemeViewerScreenState extends State { var style = entry.value; return DataRow(cells: [ DataCell(Text(name)), - DataCell(Text(style.fontSize.toString())), - DataCell(Text(style.fontWeight.toString().replaceFirst('FontWeight.w', ''))), + DataCell(Text(style?.fontSize?.toString() ?? '')), + DataCell(Text(style?.fontWeight?.toString().replaceFirst('FontWeight.w', '') ?? '')), DataCell(Row( children: [ Container( child: Container( width: 20, height: 20, - color: style.color, + color: style?.color, ), //color: bgColor, padding: EdgeInsets.all(4), @@ -406,8 +412,8 @@ class _ThemeViewerScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('#' + style.color.value.toRadixString(16).substring(2).toUpperCase()), - Text((100 * style.color.opacity).toStringAsFixed(0) + '% opacity') + Text('#' + (style?.color?.value.toRadixString(16).substring(2).toUpperCase() ?? '')), + Text((100 * (style?.color?.opacity ?? 1)).toStringAsFixed(0) + '% opacity') ], ) ], diff --git a/apps/flutter_parent/lib/screens/under_construction_screen.dart b/apps/flutter_parent/lib/screens/under_construction_screen.dart index d9d90106fb..72286bcd9f 100644 --- a/apps/flutter_parent/lib/screens/under_construction_screen.dart +++ b/apps/flutter_parent/lib/screens/under_construction_screen.dart @@ -20,7 +20,7 @@ import 'package:flutter_svg/svg.dart'; class UnderConstructionScreen extends StatelessWidget { final bool showAppBar; - const UnderConstructionScreen({this.showAppBar = false, Key key}) : super(key: key); + const UnderConstructionScreen({this.showAppBar = false, super.key}); @override Widget build(BuildContext context) { @@ -30,7 +30,7 @@ class UnderConstructionScreen extends StatelessWidget { elevation: 0, backgroundColor: Colors.transparent, iconTheme: Theme.of(context).iconTheme, - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ) : null, body: _body(context), @@ -60,7 +60,7 @@ class UnderConstructionScreen extends StatelessWidget { Text( L10n(context).currentlyBuildingThisFeature, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.subtitle1.copyWith(fontWeight: FontWeight.normal), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal), ), ], ), diff --git a/apps/flutter_parent/lib/screens/web_login/web_login_interactor.dart b/apps/flutter_parent/lib/screens/web_login/web_login_interactor.dart index b59f92d346..b904738c9d 100644 --- a/apps/flutter_parent/lib/screens/web_login/web_login_interactor.dart +++ b/apps/flutter_parent/lib/screens/web_login/web_login_interactor.dart @@ -19,20 +19,20 @@ import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class WebLoginInteractor { - Future mobileVerify(String domain) { + Future mobileVerify(String domain) { return locator().mobileVerify(domain); } - Future performLogin(MobileVerifyResult result, String oAuthRequest) async { + Future performLogin(MobileVerifyResult? result, String oAuthRequest) async { final tokens = await locator().getTokens(result, oAuthRequest); Login login = Login((b) => b - ..accessToken = tokens.accessToken - ..refreshToken = tokens.refreshToken - ..domain = result.baseUrl - ..clientId = result.clientId - ..clientSecret = result.clientSecret - ..user = tokens.user.toBuilder()); + ..accessToken = tokens?.accessToken + ..refreshToken = tokens?.refreshToken + ..domain = result?.baseUrl + ..clientId = result?.clientId + ..clientSecret = result?.clientSecret + ..user = tokens?.user?.toBuilder()); ApiPrefs.addLogin(login); ApiPrefs.switchLogins(login); diff --git a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart index 8d73ea6485..1b8d04272c 100644 --- a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart +++ b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart @@ -14,7 +14,7 @@ import 'dart:async'; -import 'package:device_info/device_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/mobile_verify_result.dart'; @@ -25,7 +25,6 @@ import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/web_login/web_login_interactor.dart'; import 'package:flutter_parent/utils/common_widgets/arrow_aware_focus_scope.dart'; import 'package:flutter_parent/utils/common_widgets/loading_indicator.dart'; -import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/service_locator.dart'; @@ -45,14 +44,14 @@ class WebLoginScreen extends StatefulWidget { this.pass, this.authenticationProvider, this.loginFlow = LoginFlow.normal, - Key key, - }) : super(key: key); + super.key, + }); - final String user; - final String accountName; - final String pass; + final String? user; + final String? accountName; + final String? pass; final String domain; - final String authenticationProvider; + final String? authenticationProvider; final LoginFlow loginFlow; static const String PROTOCOL_SKIP_VERIFY_KEY = 'skip-protocol'; @@ -71,12 +70,13 @@ class _WebLoginScreenState extends State { WebLoginInteractor get _interactor => locator(); - Future _verifyFuture; - WebViewController _controller; - String _authUrl; - String _domain; + Future? _verifyFuture; + WebViewController? _controller; + late String _authUrl; + late String _domain; bool _showLoading = false; bool _isMobileVerifyError = false; + bool loadStarted = false; @override Widget build(BuildContext context) { @@ -84,7 +84,7 @@ class _WebLoginScreenState extends State { builder: (context) => Scaffold( appBar: AppBar( title: Text(widget.domain), - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), ), body: _loginBody(), // MBL-14271: When in landscape mode, set this to false in order to avoid the situation @@ -99,18 +99,19 @@ class _WebLoginScreenState extends State { _verifyFuture = (widget.loginFlow == LoginFlow.skipMobileVerify) ? Future.delayed(Duration.zero, () => _SkipVerifyDialog.asDialog(context, widget.domain)).then((result) { // Use the result if we have it, otherwise continue on with mobile verify - return result ?? _interactor.mobileVerify(widget.domain); - }) + if (result != null) { return result; } + return _interactor.mobileVerify(widget.domain); + }) : _interactor.mobileVerify(widget.domain); } return FutureBuilder( future: _verifyFuture, - builder: (context, AsyncSnapshot snapshot) { + builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return LoadingIndicator(); } else { - _isMobileVerifyError = snapshot.hasError || (snapshot.hasData && snapshot.data.result != VerifyResultEnum.success); + _isMobileVerifyError = snapshot.hasError || (snapshot.hasData && snapshot.data!.result != VerifyResultEnum.success); if (_isMobileVerifyError) { WidgetsBinding.instance.addPostFrameCallback((_) { _showErrorDialog(context, snapshot); @@ -124,7 +125,7 @@ class _WebLoginScreenState extends State { ); } - Widget _webView(BuildContext context, AsyncSnapshot snapshot) { + Widget _webView(BuildContext context, AsyncSnapshot snapshot) { final verifyResult = snapshot.data; return Stack( @@ -133,40 +134,37 @@ class _WebLoginScreenState extends State { navigationDelegate: (request) => _navigate(context, request, verifyResult), javascriptMode: JavascriptMode.unrestricted, - darkMode: ParentTheme - .of(context) - .isWebViewDarkMode, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode, userAgent: ApiPrefs.getUserAgent(), onPageFinished: (url) => _pageFinished(url, verifyResult), - onPageStarted: (url) => _showLoadingState(), + onPageStarted: (url) => _pageStarted(url), onWebViewCreated: (controller) => - _webViewCreated(controller, verifyResult) + _webViewCreated(controller, verifyResult), ), - if (_showLoading) LoadingIndicator(), + if (_showLoading) ...[ + Container(color: Theme.of(context).scaffoldBackgroundColor), + LoadingIndicator(), + ], ], ); } - void _webViewCreated(WebViewController controller, MobileVerifyResult verifyResult) async { + void _webViewCreated(WebViewController controller, MobileVerifyResult? verifyResult) async { controller.clearCache(); _controller = controller; // WebView's created, time to load await _buildAuthUrl(verifyResult); - await _loadAuthUrl(); + _loadAuthUrl(); if (!_controllerCompleter.isCompleted) _controllerCompleter.complete(controller); } - void _pageFinished(String url, MobileVerifyResult verifyResult) { - if (!_isMobileVerifyError) { - setState(() => _showLoading = false); - } - + void _pageFinished(String url, MobileVerifyResult? verifyResult) { _controllerCompleter.future.then((controller) async { if (widget.user != null && widget.pass != null) { // SnickerDoodle login - controller.evaluateJavascript("""javascript: { + await controller.evaluateJavascript("""javascript: { document.getElementsByName('pseudonym_session[unique_id]')[0].value = '${widget.user}'; document.getElementsByName('pseudonym_session[password]')[0].value = '${widget.pass}'; document.getElementsByClassName('Button')[0].click(); @@ -180,20 +178,34 @@ class _WebLoginScreenState extends State { (function() { return (''+document.getElementsByTagName('html')[0].innerHTML+''); })(); """); if (htmlError != null && htmlError.contains("redirect_uri does not match client settings")) { - _buildAuthUrl(verifyResult, forceAuthRedirect: true); + await _buildAuthUrl(verifyResult, forceAuthRedirect: true); controller.loadUrl("about:blank"); _loadAuthUrl(); } + if (loadStarted) { + _hideLoadingDialog(); + } }); } + void _pageStarted(String url) { + loadStarted = true; + _showLoadingState(); + } + void _showLoadingState() { if (!_isMobileVerifyError) { setState(() => _showLoading = true); } } - NavigationDecision _navigate(BuildContext context, NavigationRequest request, MobileVerifyResult result) { + void _hideLoadingDialog() { + if (!_isMobileVerifyError) { + setState(() => _showLoading = false); + } + } + + NavigationDecision _navigate(BuildContext context, NavigationRequest request, MobileVerifyResult? result) { if (request.url.contains(SUCCESS_URL)) { // Success! Try to get tokens now var url = request.url; @@ -201,7 +213,7 @@ class _WebLoginScreenState extends State { locator().performLogin(result, oAuthRequest).then((_) { locator().logEvent( AnalyticsEventConstants.LOGIN_SUCCESS, - extras: {AnalyticsParamConstants.DOMAIN_PARAM: result.baseUrl}, + extras: {AnalyticsParamConstants.DOMAIN_PARAM: result?.baseUrl}, ); final lastAccount = new SchoolDomain((builder) => builder @@ -213,7 +225,7 @@ class _WebLoginScreenState extends State { }).catchError((_) { locator().logEvent( AnalyticsEventConstants.LOGIN_FAILURE, - extras: {AnalyticsParamConstants.DOMAIN_PARAM: result.baseUrl}, + extras: {AnalyticsParamConstants.DOMAIN_PARAM: result?.baseUrl}, ); // Load the original auth url so the user can try again _loadAuthUrl(); @@ -248,26 +260,26 @@ class _WebLoginScreenState extends State { } /// Sets an authenticated login url as well as the base url of the institution - void _buildAuthUrl( - MobileVerifyResult verifyResult, { + Future _buildAuthUrl( + MobileVerifyResult? verifyResult, { bool forceAuthRedirect = false, }) async { // Sanitize the url - String baseUrl = verifyResult?.baseUrl; + String? baseUrl = verifyResult?.baseUrl; if ((baseUrl?.length ?? 0) == 0) { baseUrl = widget.domain; } - if (baseUrl.endsWith('/')) { - baseUrl = baseUrl.substring(0, baseUrl.length - 1); + if (baseUrl?.endsWith('/') == true) { + baseUrl = baseUrl!.substring(0, baseUrl.length - 1); } - final scheme = Uri.parse(baseUrl).scheme; + final scheme = baseUrl == null ? null : Uri.parse(baseUrl).scheme; if (scheme == null || scheme.isEmpty) { baseUrl = 'https://${baseUrl}'; } // Prepare login information var purpose = await DeviceInfoPlugin().androidInfo.then((info) => info.model.replaceAll(' ', '_')); - var clientId = verifyResult != null ? Uri.encodeQueryComponent(verifyResult?.clientId) : ''; + var clientId = verifyResult != null ? Uri.encodeQueryComponent(verifyResult.clientId) : ''; var redirect = Uri.encodeQueryComponent('https://canvas.instructure.com/login/oauth2/auth'); if (forceAuthRedirect || widget.domain.contains(".test.") || widget.loginFlow == LoginFlow.skipMobileVerify) { @@ -280,28 +292,28 @@ class _WebLoginScreenState extends State { // If an authentication provider is supplied we need to pass that along. This should only be appended if one exists. if (widget.authenticationProvider != null && - widget.authenticationProvider.length > 0 && - widget.authenticationProvider.toLowerCase() != 'null') { + widget.authenticationProvider!.length > 0 && + widget.authenticationProvider!.toLowerCase() != 'null') { locator().logMessage('authentication_provider=${widget.authenticationProvider}'); - result = '$result&authentication_provider=${Uri.encodeQueryComponent(widget.authenticationProvider)}'; + result = '$result&authentication_provider=${Uri.encodeQueryComponent(widget.authenticationProvider!)}'; } if (widget.loginFlow == LoginFlow.canvas) result += '&canvas_login=1'; // Set the variables to use when doing a load _authUrl = result; - _domain = baseUrl; + _domain = baseUrl ?? ''; } /// Shows a simple alert dialog with an error message that correlates to the result code - _showErrorDialog(BuildContext context, AsyncSnapshot snapshot) => showDialog( + _showErrorDialog(BuildContext context, AsyncSnapshot snapshot) => showDialog( context: context, builder: (context) { return AlertDialog( title: Text(L10n(context).unexpectedError), content: Text(_getErrorMessage(context, snapshot)), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).ok), onPressed: () => Navigator.of(context).pop(), ), @@ -309,7 +321,7 @@ class _WebLoginScreenState extends State { ); }); - String _getErrorMessage(BuildContext context, AsyncSnapshot snapshot) { + String _getErrorMessage(BuildContext context, AsyncSnapshot snapshot) { final localizations = L10n(context); // No data means the request failed for some other reason that we don't know @@ -318,7 +330,7 @@ class _WebLoginScreenState extends State { return localizations.domainVerificationErrorUnknown; } - switch (snapshot.data.result) { + switch (snapshot.data!.result) { case VerifyResultEnum.generalError: return localizations.domainVerificationErrorGeneral; case VerifyResultEnum.domainNotAuthorized: @@ -335,12 +347,12 @@ class _WebLoginScreenState extends State { class _SkipVerifyDialog extends StatefulWidget { final String domain; - const _SkipVerifyDialog(this.domain, {Key key}) : super(key: key); + const _SkipVerifyDialog(this.domain, {super.key}); @override __SkipVerifyDialogState createState() => __SkipVerifyDialogState(); - static Future asDialog(BuildContext context, String domain) { + static Future asDialog(BuildContext context, String domain) { return showDialog(context: context, builder: (_) => _SkipVerifyDialog(domain)); } } @@ -369,11 +381,11 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { title: Text(L10n(context).skipMobileVerifyTitle), // Non translated string content: _content(), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).cancel.toUpperCase()), onPressed: () => Navigator.of(context).pop(null), ), - FlatButton( + TextButton( child: Text(L10n(context).ok.toUpperCase()), onPressed: () => _popWithResult(), ), @@ -382,7 +394,7 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { } void _popWithResult() { - if (_formKey.currentState.validate()) { + if (_formKey.currentState?.validate() == true) { Navigator.of(context).pop(MobileVerifyResult((b) => b ..clientId = _clientId ..clientSecret = _clientSecret @@ -401,7 +413,7 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { node: _focusScopeNode, child: Form( key: _formKey, - autovalidate: _autoValidate, + autovalidateMode: _autoValidate ? AutovalidateMode.always : AutovalidateMode.disabled, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -410,7 +422,7 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { decoration: _decoration(L10n(context).skipMobileVerifyProtocol), initialValue: _protocol, onChanged: (text) => _protocol = text, - validator: (text) => text.isEmpty ? L10n(context).skipMobileVerifyProtocolMissing : null, + validator: (text) => text?.isEmpty == true ? L10n(context).skipMobileVerifyProtocolMissing : null, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _focusScopeNode.nextFocus(), ), @@ -419,7 +431,7 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { key: Key(WebLoginScreen.ID_SKIP_VERIFY_KEY), decoration: _decoration(L10n(context).skipMobileVerifyClientId), onChanged: (text) => _clientId = text, - validator: (text) => text.isEmpty ? L10n(context).skipMobileVerifyClientIdMissing : null, + validator: (text) => text?.isEmpty == true ? L10n(context).skipMobileVerifyClientIdMissing : null, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _focusScopeNode.nextFocus(), ), @@ -428,7 +440,7 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { key: Key(WebLoginScreen.SECRET_SKIP_VERIFY_KEY), decoration: _decoration(L10n(context).skipMobileVerifyClientSecret), onChanged: (text) => _clientSecret = text, - validator: (text) => text.isEmpty ? L10n(context).skipMobileVerifyClientSecretMissing : null, + validator: (text) => text?.isEmpty == true ? L10n(context).skipMobileVerifyClientSecretMissing : null, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _focusScopeNode.nextFocus(), onEditingComplete: _popWithResult, @@ -442,7 +454,7 @@ class __SkipVerifyDialogState extends State<_SkipVerifyDialog> { InputDecoration _decoration(String label) => InputDecoration( labelText: label, - fillColor: ParentTheme.of(context).nearSurfaceColor, + fillColor: ParentTheme.of(context)?.nearSurfaceColor, filled: true, ); } diff --git a/apps/flutter_parent/lib/utils/alert_helper.dart b/apps/flutter_parent/lib/utils/alert_helper.dart index cdd947e54f..c4403e4b95 100644 --- a/apps/flutter_parent/lib/utils/alert_helper.dart +++ b/apps/flutter_parent/lib/utils/alert_helper.dart @@ -18,15 +18,16 @@ import 'package:flutter_parent/network/api/course_api.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class AlertsHelper { - Future> filterAlerts(List list) async { + Future?> filterAlerts(List? list) async { List filteredList = []; + if (list == null) return null; for (var element in list) { var courseId = element.getCourseIdForGradeAlerts(); if (courseId == null) { filteredList.add(element); } else { - Course course = await locator().getCourse(courseId, forceRefresh: false); - if (!(course.settings?.restrictQuantitativeData ?? false)) { + Course? course = await locator().getCourse(courseId, forceRefresh: false); + if (course?.settings?.restrictQuantitativeData == false) { filteredList.add(element); } } diff --git a/apps/flutter_parent/lib/utils/base_model.dart b/apps/flutter_parent/lib/utils/base_model.dart index 61d5b38bb2..5b8c9459e2 100644 --- a/apps/flutter_parent/lib/utils/base_model.dart +++ b/apps/flutter_parent/lib/utils/base_model.dart @@ -23,13 +23,13 @@ class BaseModel extends ChangeNotifier { ViewState get state => _state; - void setState({ViewState viewState}) { - if (viewState != null) _state = viewState; + void setState({required ViewState viewState}) { + _state = viewState; notifyListeners(); } // A helper method to set the state to busy when starting a load, and setting the state back to idle when done - Future work(Future Function() loadBlock) async { + Future work(Future Function()? loadBlock) async { try { setState(viewState: ViewState.Busy); if (loadBlock != null) await loadBlock(); diff --git a/apps/flutter_parent/lib/utils/common_widgets/arrow_aware_focus_scope.dart b/apps/flutter_parent/lib/utils/common_widgets/arrow_aware_focus_scope.dart index dfc9549d6d..164cda3d8d 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/arrow_aware_focus_scope.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/arrow_aware_focus_scope.dart @@ -42,7 +42,7 @@ FocusOnKeyCallback _onDirectionKeyCallback = (node, event) { // A FocusScope that properly handles directional-arrow presses (and dpad). class ArrowAwareFocusScope extends FocusScope { - ArrowAwareFocusScope({Widget child, FocusScopeNode node}) + ArrowAwareFocusScope({required Widget child, FocusScopeNode? node}) : super(child: child, node: node, onKey: _onDirectionKeyCallback); } diff --git a/apps/flutter_parent/lib/utils/common_widgets/attachment_indicator_widget.dart b/apps/flutter_parent/lib/utils/common_widgets/attachment_indicator_widget.dart index 426aa15758..828289e8b0 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/attachment_indicator_widget.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/attachment_indicator_widget.dart @@ -22,9 +22,9 @@ import 'package:transparent_image/transparent_image.dart'; class AttachmentIndicatorWidget extends StatelessWidget { final Attachment attachment; - final Function(Attachment) onAttachmentClicked; + final Function(Attachment)? onAttachmentClicked; - const AttachmentIndicatorWidget({Key key, @required this.attachment, this.onAttachmentClicked}) : super(key: key); + const AttachmentIndicatorWidget({required this.attachment, required this.onAttachmentClicked, super.key}); @override Widget build(BuildContext context) { @@ -50,12 +50,12 @@ class AttachmentIndicatorWidget extends StatelessWidget { children: [ Icon( attachment.getIcon(), - color: Theme.of(context).accentColor, + color: Theme.of(context).colorScheme.secondary, ), Padding( padding: const EdgeInsets.fromLTRB(12, 11, 12, 0), child: Text( - attachment.displayName, + attachment.displayName!, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -64,13 +64,13 @@ class AttachmentIndicatorWidget extends StatelessWidget { ) ], ), - if (attachment.thumbnailUrl != null && attachment.thumbnailUrl.isNotEmpty) + if (attachment.thumbnailUrl != null && attachment.thumbnailUrl!.isNotEmpty) ClipRRect( borderRadius: new BorderRadius.circular(4), child: FadeInImage.memoryNetwork( fadeInDuration: const Duration(milliseconds: 300), fit: BoxFit.cover, - image: attachment.thumbnailUrl, + image: attachment.thumbnailUrl!, placeholder: kTransparentImage, ), ), @@ -78,7 +78,7 @@ class AttachmentIndicatorWidget extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: () { - if (onAttachmentClicked != null) onAttachmentClicked(attachment); + if (onAttachmentClicked != null) onAttachmentClicked!(attachment); }, ), ), diff --git a/apps/flutter_parent/lib/utils/common_widgets/avatar.dart b/apps/flutter_parent/lib/utils/common_widgets/avatar.dart index c504d5593b..802408915c 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/avatar.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/avatar.dart @@ -18,11 +18,11 @@ import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/utils/design/parent_colors.dart'; class Avatar extends StatelessWidget { - final Color backgroundColor; - final String url; + final Color? backgroundColor; + final String? url; final double radius; - final Widget overlay; - final String name; // Generally should be the shortname of the user + final Widget? overlay; + final String? name; // Generally should be the shortname of the user final bool showInitials; static const List noPictureUrls = const [ @@ -34,19 +34,19 @@ class Avatar extends StatelessWidget { const Avatar( this.url, { - Key key, this.backgroundColor, this.radius = 20, this.overlay, this.name, this.showInitials = true, - }) : super(key: key); + super.key, + }); Avatar.fromUser( User user, { - Color backgroundColor, + Color? backgroundColor, double radius = 20, - Widget overlay, + Widget? overlay, }) : this(user.avatarUrl, name: user.shortName, backgroundColor: backgroundColor, radius: radius, overlay: overlay); @override @@ -59,7 +59,7 @@ class Avatar extends StatelessWidget { var isTest = WidgetsBinding.instance.runtimeType != WidgetsFlutterBinding; // Url is valid if it's not null or empty, does not contain any noPictureUrls, and we're not testing - bool isUrlValid = !isTest && url != null && url.isNotEmpty && !noPictureUrls.any((it) => url.contains(it)); + bool isUrlValid = !isTest && url != null && url!.isNotEmpty && !noPictureUrls.any((it) => url!.contains(it)); return Semantics( excludeSemantics: true, @@ -116,7 +116,7 @@ class Avatar extends StatelessWidget { } // This method is static to make it easier to test! - static String getUserInitials(String shortName) { + static String getUserInitials(String? shortName) { if (shortName == null || shortName.isEmpty) return '?'; var name = shortName; diff --git a/apps/flutter_parent/lib/utils/common_widgets/badges.dart b/apps/flutter_parent/lib/utils/common_widgets/badges.dart index 1cb9d5eb93..3069c0b5c7 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/badges.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/badges.dart @@ -23,15 +23,15 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; /// See Also for more details: /// * [NumberBadge] /// * [IndicatorBadge] -typedef String GetSemantics(BuildContext context, int count); +typedef String GetSemantics(BuildContext context, int? count); /// A simple class to wrap options for [NumberBadge] and [WidgetBadge] class BadgeOptions { /// The initial count to show for the badge - final int count; + final int? count; /// The max count a badge can show, for counts greater than this the string shown will be '$maxCount+' - final int maxCount; + final int? maxCount; /// True if the badge should include a border final bool includeBorder; @@ -54,18 +54,16 @@ class BadgeOptions { /// * [IndicatorBadge] for when a simple circle is all that is needed class WidgetBadge extends StatelessWidget { final Widget icon; - final BadgeOptions options; - final GetSemantics semantics; - final ValueListenable countListenable; + final BadgeOptions? options; + final GetSemantics? semantics; + final ValueListenable? countListenable; - const WidgetBadge(this.icon, {Key key, this.options = const BadgeOptions(), this.semantics, this.countListenable}) - : assert(icon != null), - super(key: key); + const WidgetBadge(this.icon, {this.options = const BadgeOptions(), this.semantics, this.countListenable, super.key}); @override Widget build(BuildContext context) { return Stack( - overflow: Overflow.visible, + clipBehavior: Clip.none, children: [ icon, _badge(), @@ -81,7 +79,7 @@ class WidgetBadge extends StatelessWidget { return PositionedDirectional( end: -10, top: -10, - child: NumberBadge(options: options, semantics: semantics, listenable: countListenable), + child: NumberBadge(options: options!, semantics: semantics, listenable: countListenable), ); } } @@ -96,26 +94,26 @@ class NumberBadge extends StatelessWidget { static final borderKey = const ValueKey('borderKey'); final BadgeOptions options; - final GetSemantics semantics; - final ValueListenable listenable; + final GetSemantics? semantics; + final ValueListenable? listenable; - const NumberBadge({Key key, this.options = const BadgeOptions(), this.semantics, this.listenable}) : super(key: key); + const NumberBadge({this.options = const BadgeOptions(), this.semantics, this.listenable, super.key}); @override Widget build(BuildContext context) { if (listenable == null) return _badge(context, options.count); return ValueListenableBuilder( - valueListenable: listenable, - builder: (context, count, _) => _badge(context, count), + valueListenable: listenable!, + builder: (context, count, _) => _badge(context, count as int?), ); } - Widget _badge(BuildContext context, int count) { + Widget _badge(BuildContext context, int? count) { // If there's no count, then don't show anything if (count == null || count <= 0) return SizedBox(); final maxCount = options.maxCount; - final accentColor = (ParentTheme.of(context).isDarkMode ? Colors.black : Theme.of(context).accentColor); + final accentColor = (ParentTheme.of(context)?.isDarkMode == true ? Colors.black : Theme.of(context).colorScheme.secondary); // Wrap in another container to get the border around the badge, since using border for circles in a box decoration // has antialiasing issues. @@ -133,10 +131,10 @@ class NumberBadge extends StatelessWidget { padding: const EdgeInsets.all(6.0), child: Text( maxCount != null && count > maxCount ? L10n(context).badgeNumberPlus(maxCount) : '$count', - semanticsLabel: semantics != null ? semantics(context, count) : L10n(context).unreadCount(count), + semanticsLabel: semantics != null ? semantics!(context, count) : L10n(context).unreadCount(count), style: TextStyle( fontSize: 10, - color: options.onPrimarySurface ? accentColor : Theme.of(context).accentIconTheme.color, + color: options.onPrimarySurface ? accentColor : Theme.of(context).scaffoldBackgroundColor, fontWeight: FontWeight.bold, ), ), @@ -151,14 +149,14 @@ class NumberBadge extends StatelessWidget { /// Defaults semantics to [AppLocalizations.unread] if not provided, can be overridden to return null so no semantics /// label is added. Never provides a value for 'count' in the semantics function. class IndicatorBadge extends StatelessWidget { - final GetSemantics semantics; + final GetSemantics? semantics; - const IndicatorBadge({Key key, this.semantics}) : super(key: key); + const IndicatorBadge({this.semantics, super.key}); @override Widget build(BuildContext context) { return Semantics( - label: semantics != null ? semantics(context, null) : L10n(context).unread, + label: semantics != null ? semantics!(context, null) : L10n(context).unread, child: Container( key: Key('unread-indicator'), width: 8, @@ -173,7 +171,7 @@ class IndicatorBadge extends StatelessWidget { /// determine what color to make the background. Decoration _badgeDecoration(BuildContext context, BadgeOptions options) => BoxDecoration( shape: BoxShape.circle, - color: options.onPrimarySurface ? Theme.of(context).primaryIconTheme.color : Theme.of(context).accentColor, + color: options.onPrimarySurface ? Theme.of(context).primaryIconTheme.color : Theme.of(context).colorScheme.secondary, // Can't use border here as there is an antialiasing issue: https://github.com/flutter/flutter/issues/13675 // border: !options.includeBorder ? null : Border.all(color: Theme.of(context).scaffoldBackgroundColor, width: 2), ); diff --git a/apps/flutter_parent/lib/utils/common_widgets/canvas_loading_indicator.dart b/apps/flutter_parent/lib/utils/common_widgets/canvas_loading_indicator.dart index d70f7e9fbc..3fb3678535 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/canvas_loading_indicator.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/canvas_loading_indicator.dart @@ -19,10 +19,10 @@ import 'package:vector_math/vector_math.dart' as Vector; class CanvasLoadingIndicator extends StatefulWidget { const CanvasLoadingIndicator({ - Key key, this.size = 48, this.color = Colors.white, - }) : super(key: key); + super.key, + }); final double size; final Color color; @@ -32,9 +32,9 @@ class CanvasLoadingIndicator extends StatefulWidget { } class _CanvasLoadingIndicatorState extends State with SingleTickerProviderStateMixin { - AnimationController _controller; - Animation _animation; - _CanvasLoadingIndicatorPainter _painter; + late AnimationController _controller; + late Animation _animation; + late _CanvasLoadingIndicatorPainter _painter; @override void initState() { @@ -93,7 +93,7 @@ class _CanvasLoadingIndicatorPainter extends CustomPainter { int _iteration = 0; // Paint used to draw each circle - Paint _circlePaint; + late Paint _circlePaint; @override void paint(Canvas canvas, Size size) { diff --git a/apps/flutter_parent/lib/utils/common_widgets/dialog_with_navigator_key.dart b/apps/flutter_parent/lib/utils/common_widgets/dialog_with_navigator_key.dart index 4c1dbbd7bd..5c57214d2a 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/dialog_with_navigator_key.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/dialog_with_navigator_key.dart @@ -17,21 +17,19 @@ import 'package:flutter/material.dart'; /// _DialogRoute copied (with minor changes) from flutter/lib/src/widget/routes.dart class _DialogRoute extends PopupRoute { _DialogRoute({ - @required RoutePageBuilder pageBuilder, + required pageBuilder, + required String barrierLabel, + required RouteTransitionsBuilder transitionBuilder, bool barrierDismissible = true, - String barrierLabel, Color barrierColor = const Color(0x80000000), Duration transitionDuration = const Duration(milliseconds: 200), - RouteTransitionsBuilder transitionBuilder, - RouteSettings settings, - }) : assert(barrierDismissible != null), - _pageBuilder = pageBuilder, + super.settings, + }) : _pageBuilder = pageBuilder, _barrierDismissible = barrierDismissible, _barrierLabel = barrierLabel, _barrierColor = barrierColor, _transitionDuration = transitionDuration, - _transitionBuilder = transitionBuilder, - super(settings: settings); + _transitionBuilder = transitionBuilder; final RoutePageBuilder _pageBuilder; @@ -76,30 +74,28 @@ class _DialogRoute extends PopupRoute { /// Similar to [showDialog], but instead of taking a [BuildContext] this takes a [GlobalKey] of type [NavigatorState]. /// This is useful in situations where [showDialog] will not work because a [Navigator] is not accessible via the /// available [BuildContext], such as the masquerading UI which is an ancestor of the [Navigator]. -Future showDialogWithNavigatorKey({ - @required GlobalKey navKey, - @required WidgetBuilder builder, +Future showDialogWithNavigatorKey({ + required GlobalKey navKey, + required WidgetBuilder builder, + required BuildContext buildContext, bool barrierDismissible = true, }) { - assert(navKey != null); - assert(builder != null); - assert(barrierDismissible != null); - BuildContext context = navKey.currentContext; + BuildContext context = navKey.currentContext ?? buildContext; assert(debugCheckHasMaterialLocalizations(context)); var route = _DialogRoute( pageBuilder: (_, __, ___) => SafeArea(child: Builder(builder: builder)), - barrierDismissible: barrierDismissible, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - barrierColor: Colors.black54, - transitionDuration: const Duration(milliseconds: 150), transitionBuilder: (_, animation, __, child) { return FadeTransition( opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child, ); }, + barrierDismissible: barrierDismissible, + barrierColor: Colors.black54, + transitionDuration: const Duration(milliseconds: 150), ); - return navKey.currentState.push(route); + return (navKey.currentState)?.push(route) ?? Future.value(null); } diff --git a/apps/flutter_parent/lib/utils/common_widgets/dropdown_arrow.dart b/apps/flutter_parent/lib/utils/common_widgets/dropdown_arrow.dart index c35d2932a6..1fc71509a0 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/dropdown_arrow.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/dropdown_arrow.dart @@ -19,14 +19,13 @@ import 'package:flutter/material.dart'; /// A widget that renders a dropdown arrow which can animate between its 'collapsed' and 'expanded' states. class DropdownArrow extends StatelessWidget { const DropdownArrow({ - Key key, this.size = 4, - double strokeWidth, + double? strokeWidth, this.color = Colors.white, this.rotate = false, this.specificProgress = null, - }) : this.strokeWidth = strokeWidth ?? size / 2, - super(key: key); + super.key, + }) : this.strokeWidth = strokeWidth ?? size / 2; /// Specifies the height of the dropdown arrow. The width will always be twice this value. final double size; @@ -48,13 +47,13 @@ class DropdownArrow extends StatelessWidget { /// This is useful for cases where the expand/collapse progress is manually tracked for elements associated with /// this dropdown arrow, e.g. the calendar widget where the user can swipe vertically to expand/collapse /// between a month view and a week view. - final double specificProgress; + final double? specificProgress; @override Widget build(BuildContext context) { if (specificProgress != null) { return Transform.rotate( - angle: specificProgress * -pi, + angle: specificProgress! * -pi, child: CustomPaint( child: SizedBox(width: size * 2, height: size), painter: _DropdownArrowPainter(color, strokeWidth), @@ -85,7 +84,7 @@ class _DropdownArrowPainter extends CustomPainter { ..color = color; } - Paint _arrowPaint; + late Paint _arrowPaint; @override void paint(Canvas canvas, Size size) { diff --git a/apps/flutter_parent/lib/utils/common_widgets/empty_panda_widget.dart b/apps/flutter_parent/lib/utils/common_widgets/empty_panda_widget.dart index 7e3c626a57..de212d699c 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/empty_panda_widget.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/empty_panda_widget.dart @@ -20,59 +20,61 @@ import 'package:flutter_svg/svg.dart'; /// A simple empty widget that shows a centered SVG above a title/subtitle. All components are optionally, though /// ideally all are present. Spacing is added based on which components are present. class EmptyPandaWidget extends StatelessWidget { - final String svgPath; - final String title; - final String subtitle; - final String buttonText; - final GestureTapCallback onButtonTap; - final Widget header; + final String? svgPath; + final String? title; + final String? subtitle; + final String? buttonText; + final GestureTapCallback? onButtonTap; + final Widget? header; const EmptyPandaWidget({ - Key key, this.svgPath, this.title, this.subtitle, this.buttonText, this.onButtonTap, this.header, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { return FullScreenScrollContainer( - header: header, children: [ - if (svgPath != null) SvgPicture.asset(svgPath, excludeFromSemantics: true), + if (svgPath != null) SvgPicture.asset(svgPath!, excludeFromSemantics: true), if (svgPath != null && (title != null || subtitle != null)) SizedBox(height: 64), if (title != null) Text( - title, + title!, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 20, fontWeight: FontWeight.normal), + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.normal), ), if (title != null && subtitle != null) SizedBox(height: 8), if (subtitle != null) Text( - subtitle, + subtitle!, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.subtitle1.copyWith(fontWeight: FontWeight.normal), + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal), ), if (buttonText != null) Padding( padding: const EdgeInsets.only(top: 48), - child: FlatButton( + child: TextButton( onPressed: onButtonTap, child: Text( - buttonText, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16), - ), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(4), - side: BorderSide(color: ParentColors.tiara), + buttonText!, + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16), ), + style: TextButton.styleFrom( + shape:RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(4), + side: BorderSide(color: ParentColors.tiara), + ), ), ), + ), ], + header: header, ); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/error_panda_widget.dart b/apps/flutter_parent/lib/utils/common_widgets/error_panda_widget.dart index 445614b93c..2d3e772b30 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/error_panda_widget.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/error_panda_widget.dart @@ -24,16 +24,15 @@ import 'package:flutter_parent/utils/design/parent_colors.dart'; /// Use the [callback] to set what happens when the retry button is pressed /// Use the [errorString] to specify what was supposed to be loaded class ErrorPandaWidget extends StatelessWidget { - final Function callback; + final Function? callback; final String errorString; - final Widget header; + final Widget? header; ErrorPandaWidget(this.errorString, this.callback, {this.header}); @override Widget build(BuildContext context) { return FullScreenScrollContainer( - header: header, children: [ Icon(CanvasIcons.warning, size: 40, color: ParentColors.failure), Padding( @@ -41,20 +40,23 @@ class ErrorPandaWidget extends StatelessWidget { child: Text( errorString, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16), ), ), - FlatButton( + TextButton( onPressed: () { - callback(); + if (callback != null) callback!(); }, - child: Text(L10n(context).retry, style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16)), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(4.0), - side: BorderSide(color: ParentColors.tiara), - ), + child: Text(L10n(context).retry, style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16)), + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(4.0), + side: BorderSide(color: ParentColors.tiara), + ), + ) ) ], + header: header, ); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_dialog.dart b/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_dialog.dart index eed2652c84..81ac8a8f37 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_dialog.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_dialog.dart @@ -11,15 +11,14 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:device_info/device_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/utils/common_widgets/error_report/error_report_interactor.dart'; import 'package:flutter_parent/utils/common_widgets/full_screen_scroll_container.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/service_locator.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import '../arrow_aware_focus_scope.dart'; @@ -29,9 +28,9 @@ class ErrorReportDialog extends StatefulWidget { static const Key emailKey = Key('email'); final String title; // Used to specify different titles depending on how this dialog was shown - final String subject; - final ErrorReportSeverity severity; - final FlutterErrorDetails error; + final String? subject; + final ErrorReportSeverity? severity; + final FlutterErrorDetails? error; final bool includeEmail; // Used when shown during login, so that users can get responses from created service tickets final bool hideSeverityPicker; @@ -42,19 +41,20 @@ class ErrorReportDialog extends StatefulWidget { this.includeEmail, this.hideSeverityPicker, this.error, { - Key key, - }) : super(key: key); + super.key, + }); @override _ErrorReportDialogState createState() => _ErrorReportDialogState(); - static Future asDialog(BuildContext context, - {String title, - String subject, - ErrorReportSeverity severity, + static Future asDialog( + BuildContext context, { + String? title, + String? subject, + ErrorReportSeverity? severity, bool includeEmail = false, bool hideSeverityPicker = false, - FlutterErrorDetails error}) { + FlutterErrorDetails? error}) { return showDialog( context: context, builder: (context) => ErrorReportDialog._internal( @@ -74,13 +74,13 @@ class _ErrorReportDialogState extends State { // Non state changing variables FocusScopeNode _focusScopeNode = FocusScopeNode(); - String _subject; - String _email; - String _description; + String? _subject; + String? _email; + String? _description; // State changing variables - ErrorReportSeverity _selectedSeverity; - bool _autoValidate; + late ErrorReportSeverity? _selectedSeverity; + late bool _autoValidate; @override void initState() { @@ -106,14 +106,14 @@ class _ErrorReportDialogState extends State { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), title: Text(widget.title), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).cancel.toUpperCase()), onPressed: () => Navigator.of(context).pop(), ), - FlatButton( + TextButton( child: Text(L10n(context).sendReport.toUpperCase()), onPressed: () async { - if (_formKey.currentState.validate()) { + if (_formKey.currentState?.validate() == true) { await _submitReport(); Navigator.of(context).pop(); } else { @@ -134,7 +134,7 @@ class _ErrorReportDialogState extends State { return SingleChildScrollView( child: Form( key: _formKey, - autovalidate: _autoValidate, + autovalidateMode: _autoValidate ? AutovalidateMode.always : AutovalidateMode.disabled, child: ArrowAwareFocusScope( node: _focusScopeNode, child: Column( @@ -145,7 +145,7 @@ class _ErrorReportDialogState extends State { key: ErrorReportDialog.subjectKey, initialValue: _subject, decoration: _decoration(L10n(context).reportProblemSubject), - validator: (text) => text.isEmpty ? L10n(context).reportProblemSubjectEmpty : null, + validator: (text) => text?.isEmpty == true ? L10n(context).reportProblemSubjectEmpty : null, onChanged: (text) => _subject = text, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _focusScopeNode.nextFocus(), @@ -156,7 +156,7 @@ class _ErrorReportDialogState extends State { key: ErrorReportDialog.emailKey, decoration: _decoration(L10n(context).reportProblemEmail), validator: (text) => - (widget.includeEmail && text.isEmpty) ? L10n(context).reportProblemEmailEmpty : null, + (widget.includeEmail && text?.isEmpty == true) ? L10n(context).reportProblemEmailEmpty : null, onChanged: (text) => _email = text, textInputAction: TextInputAction.next, onFieldSubmitted: (_) => _focusScopeNode.nextFocus(), @@ -167,21 +167,21 @@ class _ErrorReportDialogState extends State { minLines: 3, maxLines: null, decoration: _decoration(L10n(context).reportProblemDescription, alignLabelWithHint: true), - validator: (text) => text.isEmpty ? L10n(context).reportProblemDescriptionEmpty : null, + validator: (text) => text?.isEmpty == true ? L10n(context).reportProblemDescriptionEmpty : null, onChanged: (text) => _description = text, ), SizedBox(height: 16), if (!widget.hideSeverityPicker) Text(L10n(context).reportProblemSeverity), if (!widget.hideSeverityPicker) Container( - color: ParentTheme.of(context).nearSurfaceColor, + color: ParentTheme.of(context)?.nearSurfaceColor, padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: DropdownButton<_SeverityOption>( itemHeight: null, isExpanded: true, underline: SizedBox(), onChanged: (option) async { - setState(() => _selectedSeverity = option.severity); + setState(() => _selectedSeverity = option?.severity); // Clear focus here, as it can go back to the text forms if they were previously selected // NO! This messes up dpad-nav //_focusScopeNode.requestFocus(FocusNode()); @@ -201,15 +201,15 @@ class _ErrorReportDialogState extends State { _submitReport() async { final l10n = L10n(context); - final info = await Future.wait([PackageInfo.fromPlatform(), DeviceInfoPlugin().androidInfo]); - PackageInfo package = info[0]; - AndroidDeviceInfo device = info[1]; + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); // Add device and package info before the users description final comment = '' + - '${l10n.device}: ${device.manufacturer} ${device.model}\n' + - '${l10n.osVersion}: Android ${device.version.release}\n' + - '${l10n.versionNumber}: ${package.appName} v${package.version} (${package.buildNumber})\n\n' + + '${l10n.device}: ${androidInfo.manufacturer} ${androidInfo.model}\n' + + '${l10n.osVersion}: Android ${androidInfo.version.release}\n' + + '${l10n.versionNumber}: ${packageInfo.appName} v${packageInfo.version} (${packageInfo.buildNumber})\n\n' + '-------------------------\n\n' + '$_description'; @@ -221,7 +221,7 @@ class _ErrorReportDialogState extends State { InputDecoration _decoration(String label, {bool alignLabelWithHint = false}) => InputDecoration( labelText: label, alignLabelWithHint: alignLabelWithHint, - fillColor: ParentTheme.of(context).nearSurfaceColor, + fillColor: ParentTheme.of(context)?.nearSurfaceColor, filled: true, ); diff --git a/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_interactor.dart b/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_interactor.dart index 57ee897187..bcb2fc523b 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_interactor.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/error_report/error_report_interactor.dart @@ -19,14 +19,14 @@ import 'package:flutter_parent/utils/service_locator.dart'; class ErrorReportInteractor { Future submitErrorReport( - String subject, String description, String email, ErrorReportSeverity severity, String stacktrace) async { + String? subject, String description, String? email, ErrorReportSeverity? severity, String? stacktrace) async { final user = ApiPrefs.getUser(); final domain = (ApiPrefs.getDomain()?.isNotEmpty == true) ? ApiPrefs.getDomain() : ErrorReportApi.DEFAULT_DOMAIN; - final becomeUser = (user?.id?.isNotEmpty == true) ? '$domain?become_user_id=${user.id}' : ''; + final becomeUser = (user?.id.isNotEmpty == true) ? '$domain?become_user_id=${user?.id}' : ''; final userEmail = (email?.isNotEmpty == true) ? email : user?.primaryEmail ?? ''; final enrollments = user == null ? [] : await locator().getSelfEnrollments(forceRefresh: true); - final userRoles = Set.from(enrollments?.map((enrollment) => enrollment.type) ?? []).toList().join(','); + final userRoles = enrollments == null ? '' : Set.from(enrollments.map((enrollment) => enrollment.type)).toList().join(','); return locator().submitErrorReport( subject: subject, @@ -41,7 +41,7 @@ class ErrorReportInteractor { ); } - String _errorReportSeverityTag(ErrorReportSeverity severity) { + String _errorReportSeverityTag(ErrorReportSeverity? severity) { switch (severity) { case ErrorReportSeverity.COMMENT: return 'just_a_comment'; @@ -53,9 +53,9 @@ class ErrorReportInteractor { return 'blocks_what_i_need_to_do'; case ErrorReportSeverity.CRITICAL: return 'extreme_critical_emergency'; + default: + throw ArgumentError('The provided severity is not supported: ${severity.toString()} not in ${ErrorReportSeverity.values.toString()}'); } - throw ArgumentError( - 'The provided severity is not supported: ${severity.toString()} not in ${ErrorReportSeverity.values.toString()}'); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/full_screen_scroll_container.dart b/apps/flutter_parent/lib/utils/common_widgets/full_screen_scroll_container.dart index 797c586f7f..607287c2a9 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/full_screen_scroll_container.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/full_screen_scroll_container.dart @@ -20,13 +20,11 @@ import 'package:flutter/material.dart'; /// Mostly pulling from the example by Flutter for SingleChildScrollView: /// https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html class FullScreenScrollContainer extends StatelessWidget { - final Widget header; + final Widget? header; final List children; final double horizontalPadding; - const FullScreenScrollContainer({@required this.children, this.header, this.horizontalPadding = 32, Key key}) - : assert(horizontalPadding != null), - super(key: key); + const FullScreenScrollContainer({required this.children, this.header, this.horizontalPadding = 32, super.key}); @override Widget build(BuildContext context) { @@ -42,7 +40,7 @@ class FullScreenScrollContainer extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - if (header != null) header, + if (header != null) header!, Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: horizontalPadding), diff --git a/apps/flutter_parent/lib/utils/common_widgets/masquerade_ui.dart b/apps/flutter_parent/lib/utils/common_widgets/masquerade_ui.dart index 0dd1131584..e5fb5cf85d 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/masquerade_ui.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/masquerade_ui.dart @@ -33,20 +33,20 @@ class MasqueradeUI extends StatefulWidget { final Widget child; final GlobalKey navKey; - const MasqueradeUI({Key key, this.child, this.navKey}) : super(key: key); + const MasqueradeUI({required this.child, required this.navKey, super.key}); @override MasqueradeUIState createState() => MasqueradeUIState(); - static MasqueradeUIState of(BuildContext context) { + static MasqueradeUIState? of(BuildContext context) { return context.findAncestorStateOfType(); } - static void showMasqueradeCancelDialog(GlobalKey navKey) { + static void showMasqueradeCancelDialog(GlobalKey navKey, BuildContext context) { bool logout = ApiPrefs.getCurrentLogin()?.isMasqueradingFromQRCode == true; - User user = ApiPrefs.getUser(); - showDialogWithNavigatorKey( - navKey: navKey, + User user = ApiPrefs.getUser()!; + showDialog( + context: navKey.currentContext ?? context, builder: (context) { AppLocalizations l10n = L10n(context); var nameText = UserName.fromUser(user).text; @@ -55,24 +55,24 @@ class MasqueradeUI extends StatefulWidget { title: Text(L10n(context).stopActAsUser), content: Text.rich(StyleSlicer.apply(messageText, [PronounSlice(user.pronouns)])), actions: [ - FlatButton( + TextButton( child: new Text(L10n(context).cancel), - onPressed: () => navKey.currentState.pop(false), + onPressed: () => navKey.currentState?.pop(false), ), - FlatButton( + TextButton( child: new Text(L10n(context).ok), onPressed: () async { if (logout) { - await ParentTheme.of(context).setSelectedStudent(null); + await ParentTheme.of(context)?.setSelectedStudent(null); await ApiPrefs.performLogout(); await FeaturesUtils.performLogout(); - MasqueradeUI.of(context).refresh(); + MasqueradeUI.of(context)?.refresh(); locator().pushRouteAndClearStack(context, PandaRouter.login()); } else { ApiPrefs.updateCurrentLogin((b) => b ..masqueradeUser = null ..masqueradeDomain = null); - Respawn.of(context).restart(); + Respawn.of(context)?.restart(); } }, ), @@ -85,7 +85,7 @@ class MasqueradeUI extends StatefulWidget { class MasqueradeUIState extends State { bool _enabled = false; - User _user; + late User _user; GlobalKey _childKey = GlobalKey(); @@ -101,7 +101,7 @@ class MasqueradeUIState extends State { bool wasEnabled = _enabled; if (ApiPrefs.isLoggedIn() && ApiPrefs.isMasquerading()) { _enabled = true; - _user = ApiPrefs.getUser(); + _user = ApiPrefs.getUser()!; } else { _enabled = false; } @@ -144,7 +144,7 @@ class MasqueradeUIState extends State { color: Colors.white, semanticLabel: L10n(context).stopActAsUser, ), - onPressed: () => MasqueradeUI.showMasqueradeCancelDialog(widget.navKey), + onPressed: () => MasqueradeUI.showMasqueradeCancelDialog(widget.navKey, context), ), ], ), diff --git a/apps/flutter_parent/lib/utils/common_widgets/rating_dialog.dart b/apps/flutter_parent/lib/utils/common_widgets/rating_dialog.dart index b87ee43611..7c574dda6a 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/rating_dialog.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/rating_dialog.dart @@ -11,8 +11,7 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:device_info/device_info.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; @@ -22,7 +21,7 @@ import 'package:flutter_parent/utils/common_widgets/full_screen_scroll_container import 'package:flutter_parent/utils/design/parent_colors.dart'; import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_parent/utils/veneers/android_intent_veneer.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import '../url_launcher.dart'; @@ -72,17 +71,17 @@ class RatingDialog extends StatefulWidget { ); } - const RatingDialog._internal({Key key}) : super(key: key); + const RatingDialog._internal({super.key}); @override _RatingDialogState createState() => _RatingDialogState(); } class _RatingDialogState extends State { - String _comment; - int _focusedStar; - int _selectedStar; - bool _sending; + late String _comment; + late int _focusedStar; + late int _selectedStar; + late bool _sending; @override void initState() { @@ -105,7 +104,7 @@ class _RatingDialogState extends State { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), title: Text(L10n(context).ratingDialogTitle), actions: [ - FlatButton( + TextButton( child: Text(L10n(context).ratingDialogDontShowAgain.toUpperCase()), onPressed: _handleDontShowAgain, ) @@ -166,10 +165,13 @@ class _RatingDialogState extends State { ), ), SizedBox(height: 8), - RaisedButton( + ElevatedButton( child: Text(L10n(context).ratingDialogSendFeedback.toUpperCase()), - color: Theme.of(context).accentColor, - textColor: Colors.white, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.white, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.white), + ), onPressed: _sending ? null : _sendFeedbackPressed, ), ], @@ -227,11 +229,12 @@ class _RatingDialogState extends State { final email = ApiPrefs.getUser()?.primaryEmail ?? ''; final domain = ApiPrefs.getDomain() ?? ''; - final info = await Future.wait([PackageInfo.fromPlatform(), DeviceInfoPlugin().androidInfo]); - PackageInfo package = info[0]; - AndroidDeviceInfo device = info[1]; + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + - final subject = l10n.ratingDialogEmailSubject(package.version); + final subject = l10n.ratingDialogEmailSubject(packageInfo.version); // Populate the email body with information about the user String emailBody = '' + @@ -240,9 +243,9 @@ class _RatingDialogState extends State { '${l10n.helpUserId} $parentId\r\n' + '${l10n.helpEmail} $email\r\n' + '${l10n.helpDomain} $domain\r\n' + - '${l10n.versionNumber}: ${package.appName} v${package.version} (${package.buildNumber})\r\n' + - '${l10n.device}: ${device.manufacturer} ${device.model}\r\n' + - '${l10n.osVersion}: Android ${device.version.release}\r\n' + + '${l10n.versionNumber}: ${packageInfo.appName} v${packageInfo.version} (${packageInfo.buildNumber})\r\n' + + '${l10n.device}: ${androidInfo.manufacturer} ${androidInfo.model}\r\n' + + '${l10n.osVersion}: Android ${androidInfo.version.release}\r\n' + '----------------------------------------------\r\n'; locator().launchEmailWithBody(subject, emailBody); diff --git a/apps/flutter_parent/lib/utils/common_widgets/respawn.dart b/apps/flutter_parent/lib/utils/common_widgets/respawn.dart index 10e30969bc..ff7514878f 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/respawn.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/respawn.dart @@ -16,14 +16,14 @@ import 'package:flutter/widgets.dart'; /// Widget that can be respawned with a clean state. To perform respawn, call Respawn.of(context).kill() class Respawn extends StatefulWidget { - final Widget child; + final Widget? child; - const Respawn({Key key, this.child}) : super(key: key); + const Respawn({this.child, super.key}); @override _RespawnState createState() => _RespawnState(); - static _RespawnState of(BuildContext context) { + static _RespawnState? of(BuildContext context) { return context.findAncestorStateOfType<_RespawnState>(); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/two_finger_double_tap_gesture_detector.dart b/apps/flutter_parent/lib/utils/common_widgets/two_finger_double_tap_gesture_detector.dart index 4e23af7426..7f906e4623 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/two_finger_double_tap_gesture_detector.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/two_finger_double_tap_gesture_detector.dart @@ -21,11 +21,11 @@ class TwoFingerDoubleTapGestureDetector extends StatelessWidget { final bool excludeFromSemantics; const TwoFingerDoubleTapGestureDetector({ - Key key, - this.child, - this.onDoubleTap, + required this.child, + required this.onDoubleTap, this.excludeFromSemantics = false, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -44,11 +44,11 @@ class TwoFingerDoubleTapGestureDetector extends StatelessWidget { } class TwoFingerDoubleTapGestureRecognizer extends MultiTapGestureRecognizer { - VoidCallback onDoubleTap; + VoidCallback? onDoubleTap; Map _downPointers = {}; Map _upPointers = {}; - DateTime _lastTwoFingerTap; + DateTime? _lastTwoFingerTap; TwoFingerDoubleTapGestureRecognizer() { onTapDown = _trackTapDown; @@ -62,7 +62,7 @@ class TwoFingerDoubleTapGestureRecognizer extends MultiTapGestureRecognizer { } void _trackTapUp(int pointer, TapUpDetails details) { - DateTime downTime = _downPointers.remove(pointer); + DateTime? downTime = _downPointers.remove(pointer); if (downTime == null) return; DateTime upTime = DateTime.now(); if (upTime.difference(downTime) < kLongPressTimeout) _upPointers[pointer] = upTime; @@ -78,10 +78,10 @@ class TwoFingerDoubleTapGestureRecognizer extends MultiTapGestureRecognizer { void _trackTwoFingerTap() { DateTime tapTime = DateTime.now(); - DateTime lastTap = _lastTwoFingerTap; + DateTime? lastTap = _lastTwoFingerTap; _lastTwoFingerTap = tapTime; if (lastTap != null && tapTime.difference(lastTap) < kDoubleTapTimeout) { - if (onDoubleTap != null) onDoubleTap(); + if (onDoubleTap != null) onDoubleTap!(); _reset(); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/user_name.dart b/apps/flutter_parent/lib/utils/common_widgets/user_name.dart index 5b989258d4..e131b15b3c 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/user_name.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/user_name.dart @@ -19,9 +19,9 @@ import 'package:flutter_parent/models/user.dart'; class UserName extends StatelessWidget { final String userName; - final String pronouns; - final TextStyle style; - final TextOverflow overflow; + final String? pronouns; + final TextStyle? style; + final TextOverflow? overflow; UserName(this.userName, this.pronouns, {this.style, this.overflow}); @@ -30,11 +30,11 @@ class UserName extends StatelessWidget { pronouns = user.pronouns; UserName.fromUserShortName(User user, {this.style = null, this.overflow = null}) - : userName = user.shortName, + : userName = user.shortName!, pronouns = user.pronouns; UserName.fromBasicUser(BasicUser user, {this.style = null, this.overflow = null}) - : userName = user.name, + : userName = user.name!, pronouns = user.pronouns; UserName.fromRecipient(Recipient recipient, {this.style = null, this.overflow = null}) @@ -42,7 +42,7 @@ class UserName extends StatelessWidget { pronouns = recipient.pronouns; String get text { - if (pronouns != null && pronouns.isNotEmpty) { + if (pronouns != null && pronouns!.isNotEmpty) { return ('$userName ($pronouns)'); } else { return userName; @@ -62,6 +62,7 @@ class UserName extends StatelessWidget { return Text.rich( span, overflow: overflow, + style: Theme.of(context).textTheme.titleMedium ); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher.dart index 037339248a..f0dc7c21a6 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher.dart @@ -27,7 +27,7 @@ class AttachmentFetcher extends StatefulWidget { final Attachment attachment; final Widget Function(BuildContext context, File file) builder; - const AttachmentFetcher({@required this.attachment, @required this.builder, Key key}) : super(key: key); + const AttachmentFetcher({required this.attachment, required this.builder, super.key}); @override _AttachmentFetcherState createState() => _AttachmentFetcherState(); @@ -35,8 +35,8 @@ class AttachmentFetcher extends StatefulWidget { class _AttachmentFetcherState extends State { final _interactor = locator(); - CancelToken _cancelToken; - Future _fileFuture; + late CancelToken _cancelToken; + late Future _fileFuture; @override void initState() { @@ -53,7 +53,7 @@ class _AttachmentFetcherState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return LoadingIndicator(); } else if (snapshot.hasData) { - return widget.builder(context, snapshot.data); + return widget.builder(context, snapshot.data!); } else { return ErrorPandaWidget(L10n(context).errorLoadingFile, () { setState(() { @@ -67,7 +67,7 @@ class _AttachmentFetcherState extends State { @override void dispose() { - _cancelToken?.cancel(); + _cancelToken.cancel(); super.dispose(); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart index 5532ed44e3..ba3385be0f 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart @@ -28,15 +28,15 @@ class AttachmentFetcherInteractor { var file = File(savePath); if (await file.exists() && await file.length() == attachment.size) return file; - return locator().downloadFile(attachment.url, savePath, cancelToken: cancelToken); + return locator().downloadFile(attachment.url!, savePath, cancelToken: cancelToken); } Future getAttachmentSavePath(Attachment attachment) async { var fileName = attachment.filename; if (fileName == null || fileName.isEmpty) { - var index = attachment.url.lastIndexOf('/'); - if (index >= 0 && index < attachment.url.length - 1) { - fileName = attachment.url.substring(attachment.url.lastIndexOf('/') + 1); + var index = attachment.url!.lastIndexOf('/'); + if (index >= 0 && index < attachment.url!.length - 1) { + fileName = attachment.url!.substring(attachment.url!.lastIndexOf('/') + 1); } else { fileName = 'file'; } diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_interactor.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_interactor.dart index 13dbbdf17c..259e78b6be 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_interactor.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_interactor.dart @@ -13,6 +13,7 @@ // along with this program. If not, see . import 'package:android_intent_plus/android_intent.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_parent/models/attachment.dart'; import 'package:flutter_parent/utils/permission_handler.dart'; import 'package:flutter_parent/utils/veneers/android_intent_veneer.dart'; @@ -34,25 +35,12 @@ class ViewAttachmentInteractor { } Future downloadFile(Attachment attachment) async { - if (!await checkStoragePermission()) return; var dirs = await locator().getExternalStorageDirectories(type: StorageDirectory.downloads); locator().enqueue( - url: attachment.url, - savedDir: dirs[0].path, + url: attachment.url!, + savedDir: dirs![0].path, showNotification: true, openFileFromNotification: true, ); } - - Future checkStoragePermission() async { - var permissionHandler = locator(); - PermissionStatus permission = await permissionHandler.checkPermissionStatus(Permission.storage); - if (permission != PermissionStatus.granted) { - var permission = await permissionHandler.requestPermission(Permission.storage); - if (permission == PermissionStatus.granted) return true; - } else { - return true; - } - return false; - } -} +} \ No newline at end of file diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_screen.dart index 6d84d880a6..e1d03666fc 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_screen.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/view_attachment_screen.dart @@ -24,7 +24,7 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/service_locator.dart'; class ViewAttachmentScreen extends StatefulWidget { - const ViewAttachmentScreen(this.attachment, {Key key}) : super(key: key); + const ViewAttachmentScreen(this.attachment, {super.key}); final Attachment attachment; @@ -44,8 +44,8 @@ class _ViewAttachmentScreenState extends State { key: scaffoldKey, appBar: AppBar( elevation: 2, - title: Text(widget.attachment.displayName ?? widget.attachment.filename), - bottom: ParentTheme.of(context).appBarDivider(), + title: Text(widget.attachment.displayName ?? widget.attachment.filename ?? ''), + bottom: ParentTheme.of(context)?.appBarDivider(), actions: [_overflowMenu()], ), body: _body(context), @@ -54,7 +54,7 @@ class _ViewAttachmentScreenState extends State { } Widget _body(BuildContext context) { - String contentType = widget.attachment.inferContentType(); + String? contentType = widget.attachment.inferContentType(); if (contentType == null) return UnknownAttachmentTypeViewer(widget.attachment); @@ -101,7 +101,7 @@ class _ViewAttachmentScreenState extends State { _openExternally() { _interactor.openExternally(widget.attachment).catchError((_) { - scaffoldKey.currentState.showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(L10n(context).noApplicationsToHandleFile), ), diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer.dart index d26262c1be..910d13a04e 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer.dart @@ -30,7 +30,7 @@ class AudioVideoAttachmentViewer extends StatefulWidget { bool get isAudio => attachment.inferContentType()?.startsWith('audio') == true; - const AudioVideoAttachmentViewer(this.attachment, {Key key}) : super(key: key); + const AudioVideoAttachmentViewer(this.attachment, {super.key}); @override _AudioVideoAttachmentViewerState createState() => _AudioVideoAttachmentViewerState(); @@ -39,12 +39,12 @@ class AudioVideoAttachmentViewer extends StatefulWidget { class _AudioVideoAttachmentViewerState extends State { static const defaultAspectRatio = 16 / 9; - VideoPlayerController _videoController; - ChewieController _chewieController; + VideoPlayerController? _videoController; + ChewieController? _chewieController; final _interactor = locator(); - Future controllerFuture; + late Future controllerFuture; @override void initState() { @@ -53,22 +53,22 @@ class _AudioVideoAttachmentViewerState extends State } Future _initController() async { - _videoController = _interactor.makeController(widget.attachment.url); + _videoController = _interactor.makeController(widget.attachment.url!); try { // Initialized the video controller so we can get the aspect ratio - await _videoController.initialize(); + await _videoController?.initialize(); } catch (e) { // Intentionally left blank. Errors will be handled by ChewieController.errorBuilder. } // Get aspect ratio from controller, fall back to 16:9 - var aspectRatio = _videoController.value?.aspectRatio; + var aspectRatio = _videoController?.value.aspectRatio; if (aspectRatio == null || aspectRatio.isNaN || aspectRatio <= 0) aspectRatio = defaultAspectRatio; // Set up controller _chewieController = ChewieController( - videoPlayerController: _videoController, + videoPlayerController: _videoController!, aspectRatio: aspectRatio, autoInitialize: true, autoPlay: true, @@ -83,7 +83,7 @@ class _AudioVideoAttachmentViewerState extends State : Container(), errorBuilder: (context, error) => _error(context), ); - return _chewieController; + return _chewieController!; } @override @@ -93,7 +93,7 @@ class _AudioVideoAttachmentViewerState extends State builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return Center( - child: Chewie(controller: snapshot.data), + child: Chewie(controller: snapshot.data!), ); } else if (snapshot.hasError) { return _error(context); diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer_interactor.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer_interactor.dart index 17784449cb..ed8baf9122 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer_interactor.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer_interactor.dart @@ -15,7 +15,7 @@ import 'package:video_player/video_player.dart'; class AudioVideoAttachmentViewerInteractor { - VideoPlayerController makeController(String url) { - return VideoPlayerController.network(url); + VideoPlayerController? makeController(String url) { + return VideoPlayerController.networkUrl(Uri.parse(url)); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/image_attachment_viewer.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/image_attachment_viewer.dart index 1f665ff9f7..49181ae179 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/image_attachment_viewer.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/image_attachment_viewer.dart @@ -23,7 +23,7 @@ import 'package:photo_view/photo_view.dart'; class ImageAttachmentViewer extends StatelessWidget { final Attachment attachment; - const ImageAttachmentViewer(this.attachment, {Key key}) : super(key: key); + const ImageAttachmentViewer(this.attachment, {super.key}); @override Widget build(BuildContext context) { @@ -34,7 +34,7 @@ class ImageAttachmentViewer extends StatelessWidget { return PhotoView.customChild( backgroundDecoration: backgroundDecoration, child: SvgPicture.network( - attachment.url, + attachment.url ?? '', placeholderBuilder: (context) => LoadingIndicator(), ), minScale: minScale, @@ -43,7 +43,7 @@ class ImageAttachmentViewer extends StatelessWidget { return PhotoView( backgroundDecoration: backgroundDecoration, - imageProvider: NetworkImage(attachment.url), + imageProvider: NetworkImage(attachment.url ?? ''), minScale: minScale, loadingBuilder: (context, imageChunkEvent) => LoadingIndicator(), errorBuilder: (context, error, stackTrace) => EmptyPandaWidget( diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/text_attachment_viewer.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/text_attachment_viewer.dart index 19546b3f69..32cfad542a 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/text_attachment_viewer.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/text_attachment_viewer.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/utils/common_widgets/view_attachment/fetcher/atta class TextAttachmentViewer extends StatefulWidget { final Attachment attachment; - const TextAttachmentViewer(this.attachment, {Key key}) : super(key: key); + const TextAttachmentViewer(this.attachment, {super.key}); @override _TextAttachmentViewerState createState() => _TextAttachmentViewerState(); diff --git a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/unknown_attachment_type_viewer.dart b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/unknown_attachment_type_viewer.dart index d5099befc1..2eea62b257 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/unknown_attachment_type_viewer.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/view_attachment/viewers/unknown_attachment_type_viewer.dart @@ -20,7 +20,7 @@ import 'package:flutter_parent/utils/common_widgets/empty_panda_widget.dart'; class UnknownAttachmentTypeViewer extends StatelessWidget { final Attachment attachment; - const UnknownAttachmentTypeViewer(this.attachment, {Key key}) : super(key: key); + const UnknownAttachmentTypeViewer(this.attachment, {super.key}); @override Widget build(BuildContext context) { diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart index df7f3fc9fe..1a1a21ee17 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart @@ -31,10 +31,10 @@ class CanvasWebView extends StatefulWidget { final bool authContentIfNecessary; /// The html content to load into the webview - final String content; + final String? content; /// The empty description to show when the provided content is blank - final String emptyDescription; + final String? emptyDescription; /// Flag to set the webview as fullscreen, otherwise it resizes to fit it's content size (used for embedding html content on a screen with other widgets) /// @@ -42,7 +42,7 @@ class CanvasWebView extends StatefulWidget { final bool fullScreen; /// If set, delays loading the page once the webview is created - final Future futureDelay; + final Future? futureDelay; /// The horizontal padding to add to the content being loaded final double horizontalPadding; @@ -51,7 +51,6 @@ class CanvasWebView extends StatefulWidget { final double initialHeight; const CanvasWebView({ - Key key, this.authContentIfNecessary = true, this.content, this.emptyDescription, @@ -59,19 +58,16 @@ class CanvasWebView extends StatefulWidget { this.futureDelay = null, this.horizontalPadding = 0, this.initialHeight = 1, - }) : assert(initialHeight != null), - assert(horizontalPadding != null), - assert(authContentIfNecessary != null), - assert(fullScreen != null), - super(key: key); + super.key, + }); @override _CanvasWebViewState createState() => _CanvasWebViewState(); } class _CanvasWebViewState extends State { - String _content; - Future _contentFuture; + String? _content; + Future? _contentFuture; WebContentInteractor get _interactor => locator(); @@ -103,35 +99,35 @@ class _CanvasWebViewState extends State { } class _ResizingWebView extends StatefulWidget { - final Future contentFuture; - final String emptyDescription; + final Future? contentFuture; + final String? emptyDescription; final double initialHeight; final double horizontalPadding; final bool fullScreen; - final Future futureDelay; + final Future? futureDelay; const _ResizingWebView({ - Key key, this.contentFuture, - this.emptyDescription, - this.initialHeight, - this.horizontalPadding, - this.fullScreen, - this.futureDelay, - }) : super(key: key); + required this.emptyDescription, + required this.initialHeight, + required this.horizontalPadding, + required this.fullScreen, + required this.futureDelay, + super.key + }); @override _ResizingWebViewState createState() => _ResizingWebViewState(); } class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingObserver { - String _content; - WebViewController _controller; - double _height; - bool _loading; - bool _inactive; + String? _content; + WebViewController? _controller; + late double _height; + late bool _loading; + late bool _inactive; WebContentInteractor get _interactor => locator(); @@ -185,7 +181,7 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO Widget build(BuildContext context) { return FutureBuilder( future: widget.contentFuture, - builder: (context, AsyncSnapshot snapshot) => FutureBuilder( + builder: (context, AsyncSnapshot snapshot) => FutureBuilder( future: widget.futureDelay, builder: (context, delay) { // We're delaying if we're still waiting for data (besides a pull to refresh, show the previous while waiting for new data) @@ -193,7 +189,7 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO delay.connectionState == ConnectionState.waiting; // If there is no content, and we're not delaying, then we can stop showing loading since pageFinished will never get called - final emptyContent = (snapshot.data == null || snapshot.data.isEmpty); + final emptyContent = (snapshot.data == null || snapshot.data!.isEmpty); if (!delaying && emptyContent) _loading = false; return Stack( @@ -212,15 +208,15 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO ); } - Widget _contentBody(String widgetContent, bool emptyContent) { + Widget _contentBody(String? widgetContent, bool emptyContent) { // Check for empty content if (emptyContent) { - if (widget.emptyDescription == null || widget.emptyDescription.isEmpty) { + if (widget.emptyDescription?.isEmpty ?? true) { return Container(); // No empty text, so just be empty } else { return Padding( padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), - child: Text(widget.emptyDescription, style: Theme.of(context).textTheme.bodyText2), + child: Text(widget.emptyDescription ?? '', style: Theme.of(context).textTheme.bodyMedium), ); } } @@ -233,7 +229,7 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO // Handle being rebuilt by parent widgets (refresh) if (_content != widgetContent) { _height = widget.initialHeight; - _content = widgetContent; + _content = widgetContent!; _controller?.loadHtml(_content, horizontalPadding: widget.horizontalPadding); } @@ -241,7 +237,7 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO javascriptMode: JavascriptMode.unrestricted, onPageFinished: _handlePageLoaded, onWebViewCreated: _handleWebViewCreated, - darkMode: ParentTheme.of(context).isWebViewDarkMode, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode == true, navigationDelegate: _handleNavigation, gestureRecognizers: _webViewGestures(), javascriptChannels: _webViewChannels(), diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_screen.dart index 47adbba8b2..0cee461ab5 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_screen.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_screen.dart @@ -18,16 +18,16 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; class HtmlDescriptionScreen extends StatelessWidget { /// Html passed to a full screen web view - final String html; + final String? html; final String appBarTitle; - const HtmlDescriptionScreen(this.html, this.appBarTitle, {Key key}) : super(key: key); + const HtmlDescriptionScreen(this.html, this.appBarTitle, {super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - bottom: ParentTheme.of(context).appBarDivider(), + bottom: ParentTheme.of(context)?.appBarDivider(), title: Text(appBarTitle), ), body: CanvasWebView(content: html, horizontalPadding: 16), diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_tile.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_tile.dart index de60939776..579932a3a1 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_tile.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/html_description_tile.dart @@ -28,30 +28,30 @@ import 'package:flutter_parent/utils/service_locator.dart'; /// not null or empty. class HtmlDescriptionTile extends StatelessWidget { /// Html passed to a full screen web view - final String html; + final String? html; /// Only used if an emptyDescription is not null or not empty. /// Defaults to AppLocalizations.descriptionTitle - final String descriptionTitle; + final String? descriptionTitle; /// Only used if html is not null and not empty. /// Defaults to AppLocalizations.viewDescription - final String buttonLabel; + final String? buttonLabel; /// If null or empty, this will render an empty container and nothing else. - final String emptyDescription; + final String? emptyDescription; const HtmlDescriptionTile({ - Key key, - @required this.html, + required this.html, this.descriptionTitle, this.buttonLabel, this.emptyDescription, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { - if (html == null || html.isEmpty) { + if (html == null || html?.isEmpty == true) { return _buildEmptyState(context); } @@ -68,7 +68,7 @@ class HtmlDescriptionTile extends StatelessWidget { _title(context), Text( buttonLabel ?? L10n(context).viewDescription, - style: Theme.of(context).textTheme.subtitle1.copyWith(color: ParentTheme.of(context).studentColor), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: ParentTheme.of(context)?.studentColor), ), ], ), @@ -82,13 +82,13 @@ class HtmlDescriptionTile extends StatelessWidget { Widget _title(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), - child: Text(descriptionTitle ?? L10n(context).descriptionTitle, style: Theme.of(context).textTheme.overline), + child: Text(descriptionTitle ?? L10n(context).descriptionTitle, style: Theme.of(context).textTheme.labelSmall), ); } Widget _buildEmptyState(BuildContext context) { // No empty text, so just be empty - if (emptyDescription == null || emptyDescription.isEmpty) return Container(); + if (emptyDescription == null || emptyDescription!.isEmpty) return Container(); final parentTheme = ParentTheme.of(context); return Padding( @@ -102,13 +102,13 @@ class HtmlDescriptionTile extends StatelessWidget { height: 72, padding: EdgeInsets.all(16), decoration: BoxDecoration( - color: parentTheme.nearSurfaceColor, + color: parentTheme?.nearSurfaceColor, borderRadius: BorderRadius.circular(4), ), child: Center( child: Text( - emptyDescription, - style: Theme.of(context).textTheme.subtitle2.copyWith(color: parentTheme.onSurfaceColor), + emptyDescription!, + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: parentTheme?.onSurfaceColor), ), ), ), @@ -122,6 +122,6 @@ class HtmlDescriptionTile extends StatelessWidget { // may have personally identifiable information (and shouldn't be logged by the screen parameter analytics). // We'll at least get a screen view for that screen name, and a log entry, so we should still be able to track // any issues that come from navigating to this screen. - locator().push(context, HtmlDescriptionScreen(html, L10n(context).descriptionTitle)); + locator().push(context, HtmlDescriptionScreen(html!, L10n(context).descriptionTitle)); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart index 36303dc4bf..7bcbb880d5 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart @@ -22,16 +22,16 @@ import 'package:webview_flutter/webview_flutter.dart'; class SimpleWebViewScreen extends StatefulWidget { final String _url; final String _title; - final String _infoText; + final String? _infoText; - SimpleWebViewScreen(this._url, this._title, {String infoText}) : _infoText = infoText; + SimpleWebViewScreen(this._url, this._title, {String? infoText}) : _infoText = infoText; @override State createState() => _SimpleWebViewScreenState(); } class _SimpleWebViewScreenState extends State { - WebViewController _controller; + WebViewController? _controller; @override Widget build(BuildContext context) { @@ -42,13 +42,13 @@ class _SimpleWebViewScreenState extends State { elevation: 0, backgroundColor: Colors.transparent, iconTheme: Theme.of(context).iconTheme, - bottom: ParentTheme.of(context).appBarDivider(shadowInLightMode: false), - title: Text(widget._title, style: Theme.of(context).textTheme.headline6), + bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), + title: Text(widget._title, style: Theme.of(context).textTheme.titleLarge), ), body: WebView( javascriptMode: JavascriptMode.unrestricted, userAgent: ApiPrefs.getUserAgent(), - darkMode: ParentTheme.of(context).isWebViewDarkMode, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode, gestureRecognizers: Set()..add(Factory(() => WebViewGestureRecognizer())), navigationDelegate: _handleNavigation, onWebViewCreated: (controller) { @@ -68,7 +68,7 @@ class _SimpleWebViewScreenState extends State { void _handlePageLoaded(String url) async { // If there's no info to show, just return - if (widget._infoText == null || widget._infoText.isEmpty) return; + if (widget._infoText == null || widget._infoText!.isEmpty) return; // Run javascript to show the info alert await _controller?.evaluateJavascript(_showAlertJavascript); diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart index 6b77fe7359..93aaec8122 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/web_content_interactor.dart @@ -28,15 +28,15 @@ class WebContentInteractor { } Future getAuthUrl(String targetUrl) async { - if (targetUrl.contains(ApiPrefs.getDomain())) { + if (targetUrl.contains(ApiPrefs.getDomain()!)) { return _authUrl(targetUrl); } else { return targetUrl; } } - Future authContent(String content, String externalToolButtonText) async { - if (content == null || content.isEmpty) return content; + Future? authContent(String? content, String? externalToolButtonText) async { + if (content == null || content.isEmpty) return Future.value(content); String authContent = content; final iframeMatcher = RegExp(''); @@ -44,10 +44,12 @@ class WebContentInteractor { for (RegExpMatch element in matches) { final iframe = element.group(0); - if (RegExp('id=\"cnvs_content\"').hasMatch(iframe)) { - authContent = await _handleCanvasContent(iframe, authContent); - } else if (RegExp('external_tool').hasMatch(iframe)) { - authContent = await _handleLti(iframe, authContent, externalToolButtonText); + if (iframe != null) { + if (RegExp('id=\"cnvs_content\"').hasMatch(iframe)) { + authContent = await _handleCanvasContent(iframe, authContent); + } else if (RegExp('external_tool').hasMatch(iframe)) { + authContent = await _handleLti(iframe, authContent, externalToolButtonText); + } } } @@ -60,9 +62,11 @@ class WebContentInteractor { if (matcher != null) { final sourceUrl = matcher.group(1); - final authUrl = await _authUrl(sourceUrl); - final newIframe = iframe.replaceFirst(sourceUrl, authUrl); - content = content.replaceFirst(iframe, newIframe); + if (sourceUrl != null) { + final authUrl = await _authUrl(sourceUrl); + final newIframe = iframe.replaceFirst(sourceUrl, authUrl); + content = content.replaceFirst(iframe, newIframe); + } } return content; @@ -73,7 +77,7 @@ class WebContentInteractor { if (matcher != null) { final sourceUrl = matcher.group(1); // Make sure this REALLY is an LTI src, this check might need to be upgraded in the future... - if (sourceUrl.contains('external_tools')) { + if (sourceUrl != null && sourceUrl.contains('external_tools')) { final authUrl = await _authUrl(sourceUrl); final newIframe = iframe.replaceFirst(sourceUrl, authUrl); diff --git a/apps/flutter_parent/lib/utils/core_extensions/date_time_extensions.dart b/apps/flutter_parent/lib/utils/core_extensions/date_time_extensions.dart index 71fe704911..c7479dc56a 100644 --- a/apps/flutter_parent/lib/utils/core_extensions/date_time_extensions.dart +++ b/apps/flutter_parent/lib/utils/core_extensions/date_time_extensions.dart @@ -18,59 +18,59 @@ import 'package:intl/intl.dart'; /// to the 'en' locale if the current locale is unsupported. String get supportedDateLocale => DateFormat.localeExists(Intl.getCurrentLocale()) ? Intl.getCurrentLocale() : 'en'; -extension Format on DateTime { +extension Format on DateTime? { /// Formats this [DateTime] for the current locale using the provided localization function - String l10nFormat( - String Function(String date, String time) localizer, { - DateFormat dateFormat, - DateFormat timeFormat, + String? l10nFormat( + String Function(String date, String time)? localizer, { + DateFormat? dateFormat, + DateFormat? timeFormat, }) { if (this == null || localizer == null) return null; - DateTime local = toLocal(); + DateTime local = this!.toLocal(); String date = (dateFormat ?? DateFormat.MMMd(supportedDateLocale)).format(local); String time = (timeFormat ?? DateFormat.jm(supportedDateLocale)).format(local); return localizer(date, time); } - bool isSameDayAs(DateTime other) { + bool isSameDayAs(DateTime? other) { if (this == null || other == null) return false; - return this.year == other.year && this.month == other.month && this.day == other.day; + return this!.year == other.year && this!.month == other.month && this!.day == other.day; } - DateTime withFirstDayOfWeek() { + DateTime? withFirstDayOfWeek() { if (this == null) return null; final firstDay = DateFormat(null, supportedDateLocale).dateSymbols.FIRSTDAYOFWEEK; - var offset = (this.weekday - 1 - firstDay) % 7; - return DateTime(this.year, this.month, this.day - offset); + var offset = (this!.weekday - 1 - firstDay) % 7; + return DateTime(this!.year, this!.month, this!.day - offset); } - int get localDayOfWeek { + int? get localDayOfWeek { if (this == null) return null; final firstDay = DateFormat(null, supportedDateLocale).dateSymbols.FIRSTDAYOFWEEK; - return (this.weekday - 1 - firstDay) % 7; + return (this!.weekday - 1 - firstDay) % 7; } bool isWeekend() { if (this == null) return false; - return DateFormat(null, supportedDateLocale).dateSymbols.WEEKENDRANGE.contains((this.weekday - 1) % 7); + return DateFormat(null, supportedDateLocale).dateSymbols.WEEKENDRANGE.contains((this!.weekday - 1) % 7); } - DateTime withStartOfDay() => this == null ? null : DateTime(year, month, day); + DateTime? withStartOfDay() => this == null ? null : DateTime(this!.year, this!.month, this!.day); - DateTime withEndOfDay() => this == null ? null : DateTime(year, month, day, 23, 59, 59, 999); + DateTime? withEndOfDay() => this == null ? null : DateTime(this!.year, this!.month, this!.day, 23, 59, 59, 999); - DateTime withStartOfMonth() => this == null ? null : DateTime(year, month, 1); + DateTime? withStartOfMonth() => this == null ? null : DateTime(this!.year, this!.month, 1); - DateTime withEndOfMonth() => this == null ? null : DateTime(year, month + 1, 0, 23, 59, 59, 999); + DateTime? withEndOfMonth() => this == null ? null : DateTime(this!.year, this!.month + 1, 0, 23, 59, 59, 999); /// Returns this DateTime rounded to the nearest date at midnight. In other words, if the time is before noon this /// will return the same date but with the time set to midnight. If the time is at noon or after noon, this will /// return the following day at midnight. - DateTime roundToMidnight() { + DateTime? roundToMidnight() { if (this == null) { return null; - } else if (hour >= 12) { - return DateTime(year, month, day + 1); + } else if (this!.hour >= 12) { + return DateTime(this!.year, this!.month, this!.day + 1); } else { return withStartOfDay(); } diff --git a/apps/flutter_parent/lib/utils/core_extensions/list_extensions.dart b/apps/flutter_parent/lib/utils/core_extensions/list_extensions.dart index a3303c4c7e..977e676abb 100644 --- a/apps/flutter_parent/lib/utils/core_extensions/list_extensions.dart +++ b/apps/flutter_parent/lib/utils/core_extensions/list_extensions.dart @@ -27,19 +27,19 @@ enum NullSortOrder { none, } -typedef Comparable Selector(T element); +typedef Comparable? Selector(T? element); -extension ListUtils on List { +extension ListUtils on List? { /// Sorts elements in-place according to the natural sort order of the value returned by the specified [selectors]. /// Subsequent selectors will only be used when elements returned by preceding selectors have the same sort order. - List sortBy( - List> selectors, { + List? sortBySelector( + List> selectors, { bool descending = false, NullSortOrder nullSortOrder = NullSortOrder.greaterThan, }) { - if (this == null || this.isEmpty || selectors.isEmpty) return this; - sort((a, b) { - int result; + if (this == null || this?.isEmpty == true || selectors.isEmpty) return this; + this!.sort((a, b) { + int result = 0; for (int i = 0; i < selectors.length; i++) { var selector = selectors[i]; @@ -64,7 +64,6 @@ extension ListUtils on List { case NullSortOrder.none: var validValue = value1 ?? value2; throw ArgumentError('Cannot compare null to $validValue. Consider using a different NullSortOrder.'); - break; } } else { // Both values are null; treat them as equal. @@ -80,20 +79,20 @@ extension ListUtils on List { } /// Returns the number of elements matching the given [predicate]. - int count(bool Function(T) predicate) { + int count(bool Function(T?) predicate) { if (this == null) return 0; var count = 0; - this.forEach((element) { + this!.forEach((element) { if (predicate(element)) ++count; }); return count; } - List mapIndexed(R transform(int index, T t)) { + List? mapIndexed(R transform(int index, T? t)) { if (this == null) return null; final List list = []; - for (int i = 0; i < this.length; i++) { - list.add(transform(i, this[i])); + for (int i = 0; i < this!.length; i++) { + list.add(transform(i, this![i])); } return list; } diff --git a/apps/flutter_parent/lib/utils/db/calendar_filter_db.dart b/apps/flutter_parent/lib/utils/db/calendar_filter_db.dart index 3bc8937930..9fe7e5f93c 100644 --- a/apps/flutter_parent/lib/utils/db/calendar_filter_db.dart +++ b/apps/flutter_parent/lib/utils/db/calendar_filter_db.dart @@ -44,7 +44,7 @@ class CalendarFilterDb { columnFilters: joinFilters(data.filters.toSet()), }; - static CalendarFilter fromMap(Map map) => CalendarFilter((b) => b + static CalendarFilter fromMap(Map map) => CalendarFilter((b) => b ..id = map[columnId] ..userDomain = map[columnUserDomain] ..userId = map[columnUserId] @@ -76,18 +76,18 @@ class CalendarFilterDb { } } - static String joinFilters(Set filters) { + static String joinFilters(Set? filters) { if (filters == null || filters.isEmpty) return ''; return filters.join('|'); } - static Set splitFilters(String joinedFilters) { + static Set splitFilters(String? joinedFilters) { if (joinedFilters == null || joinedFilters.isEmpty) return {}; return joinedFilters.split('|').toSet(); } - Future insertOrUpdate(CalendarFilter data) async { - CalendarFilter existing = await getByObserveeId(data.userDomain, data.userId, data.observeeId); + Future insertOrUpdate(CalendarFilter data) async { + CalendarFilter? existing = await getByObserveeId(data.userDomain, data.userId, data.observeeId); if (existing == null) { var id = await db.insert(tableName, toMap(data)); return getById(id); @@ -98,13 +98,13 @@ class CalendarFilterDb { } } - Future getById(int id) async { + Future getById(int id) async { List maps = await db.query(tableName, columns: allColumns, where: '$columnId = ?', whereArgs: [id]); if (maps.isNotEmpty) return fromMap(maps.first); return null; } - Future getByObserveeId(String userDomain, String userId, String observeeId) async { + Future getByObserveeId(String userDomain, String userId, String observeeId) async { List maps = await db.query( tableName, columns: allColumns, @@ -119,7 +119,7 @@ class CalendarFilterDb { return db.delete(tableName, where: '$columnId = ?', whereArgs: [id]); } - Future deleteAllForUser(String userDomain, String userId) { + Future deleteAllForUser(String? userDomain, String? userId) { return db.delete( tableName, where: '$columnUserDomain = ? AND $columnUserId = ?', diff --git a/apps/flutter_parent/lib/utils/db/db_util.dart b/apps/flutter_parent/lib/utils/db/db_util.dart index 6f4cffc120..f894728c05 100644 --- a/apps/flutter_parent/lib/utils/db/db_util.dart +++ b/apps/flutter_parent/lib/utils/db/db_util.dart @@ -33,11 +33,11 @@ class DbUtil { static const dbVersion = 4; static const dbName = 'canvas_parent.db'; - static Database _db; + static Database? _db; static Database get instance { if (_db == null) throw StateError('DbUtil has not been initialized!'); - return _db; + return _db!; } static Future init() async { diff --git a/apps/flutter_parent/lib/utils/db/reminder_db.dart b/apps/flutter_parent/lib/utils/db/reminder_db.dart index 64b8aff061..6b660daa20 100644 --- a/apps/flutter_parent/lib/utils/db/reminder_db.dart +++ b/apps/flutter_parent/lib/utils/db/reminder_db.dart @@ -46,10 +46,10 @@ class ReminderDb { columnType: data.type, columnItemId: data.itemId, columnCourseId: data.courseId, - columnDate: data.date.toIso8601String(), + columnDate: data.date?.toIso8601String(), }; - static Reminder fromMap(Map map) => Reminder((b) => b + static Reminder fromMap(Map map) => Reminder((b) => b ..id = map[columnId] ..userDomain = map[columnUserDomain] ..userId = map[columnUserId] @@ -78,18 +78,18 @@ class ReminderDb { } } - Future insert(Reminder data) async { + Future insert(Reminder data) async { var id = await db.insert(tableName, toMap(data)); return getById(id); } - Future getById(int id) async { + Future getById(int id) async { List maps = await db.query(tableName, columns: allColumns, where: '$columnId = ?', whereArgs: [id]); if (maps.isNotEmpty) return fromMap(maps.first); return null; } - Future getByItem(String userDomain, String userId, String type, String itemId) async { + Future getByItem(String? userDomain, String? userId, String? type, String? itemId) async { List maps = await db.query( tableName, columns: allColumns, @@ -100,7 +100,7 @@ class ReminderDb { return null; } - Future> getAllForUser(String userDomain, String userId) async { + Future>? getAllForUser(String? userDomain, String? userId) async { List maps = await db.query( tableName, columns: allColumns, @@ -110,11 +110,11 @@ class ReminderDb { return maps.map((it) => fromMap(it)).toList(); } - Future deleteById(int id) { + Future deleteById(int? id) { return db.delete(tableName, where: '$columnId = ?', whereArgs: [id]); } - Future deleteAllForUser(String userDomain, String userId) { + Future deleteAllForUser(String? userDomain, String? userId) { return db.delete( tableName, where: '$columnUserDomain = ? AND $columnUserId = ?', diff --git a/apps/flutter_parent/lib/utils/db/user_colors_db.dart b/apps/flutter_parent/lib/utils/db/user_colors_db.dart index 9fdc50cfd6..79abb5ab70 100644 --- a/apps/flutter_parent/lib/utils/db/user_colors_db.dart +++ b/apps/flutter_parent/lib/utils/db/user_colors_db.dart @@ -46,7 +46,7 @@ class UserColorsDb { columnColor: userColor.color.value, }; - static UserColor fromMap(Map map) => UserColor((b) => b + static UserColor fromMap(Map map) => UserColor((b) => b ..id = map[columnId] ..userDomain = map[columnUserDomain] ..userId = map[columnUserId] @@ -71,13 +71,13 @@ class UserColorsDb { } } - Future getById(int id) async { + Future getById(int id) async { List maps = await db.query(tableName, columns: allColumns, where: '$columnId = ?', whereArgs: [id]); if (maps.isNotEmpty) return fromMap(maps.first); return null; } - Future insertOrUpdateAll(String domain, String userId, UserColors colors) async { + Future insertOrUpdateAll(String? domain, String? userId, UserColors colors) async { for (var entry in colors.customColors.entries) { await insertOrUpdate(UserColor((b) => b ..userDomain = domain @@ -89,8 +89,8 @@ class UserColorsDb { static Color parseColor(String hexCode) => Color(int.parse('FF${hexCode.substring(1)}', radix: 16)); - Future insertOrUpdate(UserColor data) async { - UserColor existing = await getByContext(data.userDomain, data.userId, data.canvasContext); + Future insertOrUpdate(UserColor data) async { + UserColor? existing = await getByContext(data.userDomain, data.userId, data.canvasContext); if (existing == null) { var id = await db.insert(tableName, toMap(data)); return getById(id); @@ -101,7 +101,7 @@ class UserColorsDb { } } - Future getByContext(String userDomain, String userId, String canvasContext) async { + Future getByContext(String? userDomain, String? userId, String canvasContext) async { List maps = await db.query( tableName, columns: allColumns, diff --git a/apps/flutter_parent/lib/utils/debouncer.dart b/apps/flutter_parent/lib/utils/debouncer.dart index 685e86dab2..ec2f92c962 100644 --- a/apps/flutter_parent/lib/utils/debouncer.dart +++ b/apps/flutter_parent/lib/utils/debouncer.dart @@ -18,13 +18,13 @@ import 'dart:async'; /// Mainly used for when large amounts of user input can cause an unnecessary large amount of network calls class Debouncer { final Duration _duration; - Timer _timer; + Timer? _timer; Debouncer(this._duration); - void debounce(Function callback) { + void debounce(void Function() callback) { if (_timer != null) { - _timer.cancel(); + _timer!.cancel(); } _timer = Timer(_duration, callback); } diff --git a/apps/flutter_parent/lib/utils/design/parent_theme.dart b/apps/flutter_parent/lib/utils/design/parent_theme.dart index e550afafa6..b5915a2c26 100644 --- a/apps/flutter_parent/lib/utils/design/parent_theme.dart +++ b/apps/flutter_parent/lib/utils/design/parent_theme.dart @@ -13,6 +13,7 @@ // along with this program. If not, see . import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_parent/models/user_color.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/utils/db/user_colors_db.dart'; @@ -43,17 +44,17 @@ class ParentTheme extends StatefulWidget { final Widget Function(BuildContext context, ThemeData themeData) builder; const ParentTheme({ - Key key, - this.builder, + required this.builder, this.themePrefs = const ThemePrefs(), - }) : super(key: key); + super.key + }); final ThemePrefs themePrefs; @override _ParentThemeState createState() => _ParentThemeState(); - static _ParentThemeState of(BuildContext context) { + static _ParentThemeState? of(BuildContext context) { return context.findAncestorStateOfType<_ParentThemeState>(); } } @@ -64,44 +65,43 @@ class ParentTheme extends StatefulWidget { class _ParentThemeState extends State { ParentThemeStateChangeNotifier _notifier = ParentThemeStateChangeNotifier(); - StudentColorSet _studentColorSet; + StudentColorSet? _studentColorSet; - String _selectedStudentId; + String? _selectedStudentId; Future refreshStudentColor() => setSelectedStudent(_selectedStudentId); /// Set the id of the selected student, used for updating the student color. Setting this to null /// effectively resets the color state. - Future setSelectedStudent(String studentId) async { + Future setSelectedStudent(String? studentId) async { _studentColorSet = null; _selectedStudentId = studentId; - if (studentId != null) { - _studentColorSet = await getColorsForStudent(studentId); - } + _studentColorSet = await getColorsForStudent(studentId); + setState(() {}); } - Future getColorsForStudent(String studentId) async { + Future getColorsForStudent(String? studentId) async { // Get saved color for this user - UserColor userColor = await locator().getByContext( + UserColor? userColor = await locator().getByContext( ApiPrefs.getDomain(), ApiPrefs.getUser()?.id, 'user_$studentId', ); StudentColorSet colorSet; - if (userColor == null) { + if (userColor == null && studentId != null) { // No saved color for this user, fall back to existing color sets based on user id var numId = studentId.replaceAll(RegExp(r'[^\d]'), ''); var index = (int.tryParse(numId) ?? studentId.length) % StudentColorSet.all.length; colorSet = StudentColorSet.all[index]; } else { // Check if there is a matching color set and prefer that for a better dark/HC mode experience - Color color = userColor.color; + Color? color = userColor?.color; colorSet = StudentColorSet.all.firstWhere( (colorSet) => colorSet.light == color, - orElse: () => StudentColorSet(color, color, color, color), + orElse: () => color != null ? StudentColorSet(color, color, color, color) : StudentColorSet.all.first, ); } return colorSet; @@ -191,10 +191,10 @@ class _ParentThemeState extends State { _appBarDivider(isDarkMode ? ParentColors.oxford : ParentColors.appBarDividerLight); /// Returns a light divider if in dark mode, dark divider in light mode unless shadowInLightMode is true, wrapping the optional bottom passed in - PreferredSizeWidget appBarDivider({PreferredSizeWidget bottom, bool shadowInLightMode = true}) => + PreferredSizeWidget? appBarDivider({PreferredSizeWidget? bottom, bool shadowInLightMode = true}) => (isDarkMode || !shadowInLightMode) ? PreferredSize( - preferredSize: Size.fromHeight(1.0 + (bottom?.preferredSize?.height ?? 0)), // Bottom height plus divider + preferredSize: Size.fromHeight(1.0 + (bottom?.preferredSize.height ?? 0)), // Bottom height plus divider child: Column( children: [ if (bottom != null) bottom, @@ -233,7 +233,7 @@ class _ParentThemeState extends State { /// Color similar to the surface color but is slightly darker in light mode and slightly lighter in dark mode. /// This should be used elements that should be visually distinguishable from the surface color but must also contrast /// sharply with the [onSurfaceColor]. Examples are chip backgrounds, progressbar backgrounds, avatar backgrounds, etc. - Color get nearSurfaceColor => isDarkMode ? Colors.grey[850] : ParentColors.porcelain; + Color get nearSurfaceColor => isDarkMode ? Colors.grey[850] ?? ParentColors.porcelain : ParentColors.porcelain; /// The green 'success' color appropriate for the current light/dark/HC mode Color get successColor => getColorVariantForCurrentState(StudentColorSet.shamrock); @@ -242,70 +242,115 @@ class _ParentThemeState extends State { var textTheme = _buildTextTheme(onSurfaceColor); // Use single text color for all styles in high-contrast mode - if (isHC) textTheme = textTheme.apply(displayColor: onSurfaceColor, bodyColor: onSurfaceColor); + if (isHC) { + textTheme = textTheme.apply(displayColor: onSurfaceColor, bodyColor: onSurfaceColor); + } + else { + textTheme = isDarkMode ? + _buildTextTheme(onSurfaceColor, fadeColor: ParentColors.ash) : + _buildTextTheme(onSurfaceColor); + } + + var primaryTextTheme = _buildTextTheme(Colors.white, fadeColor: ParentColors.tiara); + + if (isHC) { + primaryTextTheme = primaryTextTheme.apply(displayColor: Colors.white, bodyColor: Colors.white); + } else if (isDarkMode) { + primaryTextTheme = _buildTextTheme(ParentColors.porcelain, fadeColor: ParentColors.tiara); + } + + var primaryIconTheme = isDarkMode + ? IconThemeData(color: ParentColors.tiara) + : IconThemeData(color: Colors.white); + + var brightness = isDarkMode ? Brightness.dark : Brightness.light; + var backgroundColor = isDarkMode ? Colors.black : Colors.white; + var iconTheme = isDarkMode + ? IconThemeData(color: ParentColors.porcelain) + : IconThemeData(color: ParentColors.licorice); + var dividerColor = isHC ? onSurfaceColor : isDarkMode ? ParentColors + .licorice : ParentColors.tiara; + var dialogBackgroundColor = isDarkMode ? Colors.black : Colors.white; var swatch = ParentColors.makeSwatch(themeColor); + return ThemeData( - brightness: isDarkMode ? Brightness.dark : Brightness.light, primarySwatch: swatch, primaryColor: isDarkMode ? Colors.black : null, - accentColor: swatch[500], - toggleableActiveColor: swatch[500], - textSelectionHandleColor: swatch[300], - scaffoldBackgroundColor: isDarkMode ? Colors.black : Colors.white, - canvasColor: isDarkMode ? Colors.black : Colors.white, - accentColorBrightness: isDarkMode ? Brightness.light : Brightness.dark, + colorScheme: ColorScheme.fromSwatch(primarySwatch: swatch).copyWith( + secondary: swatch[500], brightness: brightness), + textSelectionTheme: TextSelectionThemeData( + selectionHandleColor: swatch[300], + ), + scaffoldBackgroundColor: backgroundColor, + canvasColor: backgroundColor, textTheme: textTheme, - primaryTextTheme: isDarkMode ? textTheme : _buildTextTheme(Colors.white, fadeColor: Colors.white70), - accentTextTheme: isDarkMode ? textTheme : _buildTextTheme(Colors.white, fadeColor: Colors.white70), - iconTheme: IconThemeData(color: onSurfaceColor), - primaryIconTheme: IconThemeData(color: isDarkMode ? ParentColors.tiara : Colors.white), - accentIconTheme: IconThemeData(color: isDarkMode ? Colors.black : Colors.white), - dividerColor: isHC ? onSurfaceColor : isDarkMode ? ParentColors.oxford : ParentColors.tiara, - buttonTheme: ButtonThemeData(height: 48, minWidth: 120, textTheme: ButtonTextTheme.primary), - fontFamily: 'Lato' + primaryTextTheme: primaryTextTheme, + iconTheme: iconTheme, + primaryIconTheme: primaryIconTheme, + dividerColor: dividerColor, + dividerTheme: DividerThemeData(color: dividerColor), + buttonTheme: ButtonThemeData( + height: 48, minWidth: 120, textTheme: ButtonTextTheme.primary), + fontFamily: 'Lato', + dialogTheme: DialogTheme( + backgroundColor: dialogBackgroundColor, + surfaceTintColor: Colors.white, + ), + tabBarTheme: TabBarTheme( + labelStyle: primaryTextTheme.titleMedium?.copyWith(fontSize: 14), + labelColor: primaryTextTheme.titleMedium?.color, + unselectedLabelStyle: primaryTextTheme.bodySmall?.copyWith( + fontSize: 14), + unselectedLabelColor: primaryTextTheme.bodySmall?.color, + ), + appBarTheme: AppBarTheme( + backgroundColor: isDarkMode ? Colors.white12 : themeColor, + foregroundColor: primaryIconTheme.color, + systemOverlayStyle: SystemUiOverlayStyle.light, + ), ); } - TextTheme _buildTextTheme(Color color, {Color fadeColor = ParentColors.ash}) { + TextTheme _buildTextTheme(Color color, {Color fadeColor = ParentColors.oxford}) { return TextTheme( /// Design-provided styles // Comments for each text style represent the nomenclature of the designs we have // Caption - subtitle2: TextStyle(color: fadeColor, fontSize: 12, fontWeight: FontWeight.w500), + titleSmall: TextStyle(color: fadeColor, fontSize: 12, fontWeight: FontWeight.w500), // Subhead - overline: TextStyle(color: fadeColor, fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 0), + labelSmall: TextStyle(color: fadeColor, fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 0), // Body - bodyText2: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.normal), + bodyMedium: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.normal), // Subtitle - caption: TextStyle(color: fadeColor, fontSize: 14, fontWeight: FontWeight.w500), + bodySmall: TextStyle(color: fadeColor, fontSize: 14, fontWeight: FontWeight.w500), // Title - subtitle1: TextStyle(color: color, fontSize: 16, fontWeight: FontWeight.w500), + titleMedium: TextStyle(color: color, fontSize: 16, fontWeight: FontWeight.w500), // Heading - headline5: TextStyle(color: color, fontSize: 18, fontWeight: FontWeight.w500), + headlineSmall: TextStyle(color: color, fontSize: 18, fontWeight: FontWeight.w500), // Display - headline4: TextStyle(color: color, fontSize: 24, fontWeight: FontWeight.w500), + headlineMedium: TextStyle(color: color, fontSize: 24, fontWeight: FontWeight.w500), /// Other/unmapped styles - headline6: TextStyle(color: color), + titleLarge: TextStyle(color: color), - headline1: TextStyle(color: fadeColor), + displayLarge: TextStyle(color: fadeColor), - headline2: TextStyle(color: fadeColor), + displayMedium: TextStyle(color: fadeColor), - headline3: TextStyle(color: fadeColor), + displaySmall: TextStyle(color: fadeColor), - bodyText1: TextStyle(color: color), + bodyLarge: TextStyle(color: color), - button: TextStyle(color: color), + labelLarge: TextStyle(color: color), ); } } @@ -327,11 +372,11 @@ class DefaultParentTheme extends StatelessWidget { final WidgetBuilder builder; final bool useNonPrimaryAppBar; - const DefaultParentTheme({Key key, @required this.builder, this.useNonPrimaryAppBar = true}) : super(key: key); + const DefaultParentTheme({required this.builder, this.useNonPrimaryAppBar = true, super.key}); @override Widget build(BuildContext context) { - var theme = ParentTheme.of(context).defaultTheme; + var theme = ParentTheme.of(context)!.defaultTheme; if (useNonPrimaryAppBar) theme = theme.copyWith(appBarTheme: _scaffoldColoredAppBarTheme(context)); return Consumer( @@ -346,8 +391,8 @@ class DefaultParentTheme extends StatelessWidget { final theme = Theme.of(context); return AppBarTheme( color: theme.scaffoldBackgroundColor, - toolbarTextStyle: theme.textTheme.bodyText2, - titleTextStyle: theme.textTheme.headline6, + toolbarTextStyle: theme.textTheme.bodyMedium, + titleTextStyle: theme.textTheme.titleLarge, iconTheme: theme.iconTheme, elevation: 0, ); diff --git a/apps/flutter_parent/lib/utils/design/theme_prefs.dart b/apps/flutter_parent/lib/utils/design/theme_prefs.dart index 7cea97971c..9b06002e67 100644 --- a/apps/flutter_parent/lib/utils/design/theme_prefs.dart +++ b/apps/flutter_parent/lib/utils/design/theme_prefs.dart @@ -22,36 +22,36 @@ class ThemePrefs { static String PREF_KEY_HC_MODE = 'high_contrast_mode'; - static SharedPreferences _prefs; + static SharedPreferences? _prefs; static Future init() async { if (_prefs == null) _prefs = await SharedPreferences.getInstance(); } @visibleForTesting - static Future clear() => _prefs.clear(); + static Future clear() => _prefs?.clear() ?? Future(() => false); const ThemePrefs(); /// Returns the stored preference for dark mode. The get the value for the theme in use, call ParentTheme.of(context).isDarkMode - bool get darkMode => _prefs.getBool(PREF_KEY_DARK_MODE) ?? false; + bool get darkMode => _prefs?.getBool(PREF_KEY_DARK_MODE) ?? false; /// Sets the dark mode value. Note that calling this only changes the stored preference. To update the theme in use, /// prefer setting ParentTheme.of(context).isDarkMode - set darkMode(bool value) => _prefs.setBool(PREF_KEY_DARK_MODE, value); + set darkMode(bool value) => _prefs?.setBool(PREF_KEY_DARK_MODE, value); /// Returns the stored preference for dark mode for WebViews. The get the value for the theme in use, /// call ParentTheme.of(context).isWebViewDarkMode - bool get webViewDarkMode => _prefs.getBool(PREF_KEY_WEB_VIEW_DARK_MODE) ?? false; + bool get webViewDarkMode => _prefs?.getBool(PREF_KEY_WEB_VIEW_DARK_MODE) ?? false; /// Sets the dark mode value for WebViews. Note that calling this only changes the stored preference. To update the /// theme in use, prefer setting ParentTheme.of(context).isWebViewDarkMode - set webViewDarkMode(bool value) => _prefs.setBool(PREF_KEY_WEB_VIEW_DARK_MODE, value); + set webViewDarkMode(bool value) => _prefs?.setBool(PREF_KEY_WEB_VIEW_DARK_MODE, value); /// Returns the stored preference for high-contrast mode. To get the value for the theme in use, call ParentTheme.of(context).isHC - bool get hcMode => _prefs.getBool(PREF_KEY_HC_MODE) ?? false; + bool get hcMode => _prefs?.getBool(PREF_KEY_HC_MODE) ?? false; /// Sets the high contrast mode value. Note that calling this only changes the stored preference. To update the theme /// in use, prefer setting ParentTheme.of(context).isHC - set hcMode(bool value) => _prefs.setBool(PREF_KEY_HC_MODE, value); + set hcMode(bool value) => _prefs?.setBool(PREF_KEY_HC_MODE, value); } diff --git a/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_overlay.dart b/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_overlay.dart index 0ba2f6a822..decaddb526 100644 --- a/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_overlay.dart +++ b/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_overlay.dart @@ -32,12 +32,12 @@ class ThemeTransitionOverlay extends StatefulWidget { this.imageBytes, this.anchorCenter, this.onReady, { - Key key, - }) : super(key: key); + super.key, + }); - static display(BuildContext context, GlobalKey anchorKey, Function() onReady) async { + static display(BuildContext context, GlobalKey? anchorKey, Function() onReady) async { // Get center of anchor, which will be the origin point of the transition animation - RenderBox box = anchorKey.currentContext.findRenderObject(); + RenderBox box = anchorKey?.currentContext!.findRenderObject() as RenderBox; var anchorCenter = box.localToGlobal(box.size.center(Offset(0, 0))); // Get the target widget over which the animation will be displayed @@ -45,11 +45,11 @@ class ThemeTransitionOverlay extends StatefulWidget { if (target == null) throw 'ThemeTransitionTarget not found in the widget tree'; // Get the repaint boundary of the target, render at 1/2 scale to a PNG image - RenderRepaintBoundary boundary = ThemeTransitionTarget.of(context).boundaryKey.currentContext.findRenderObject(); + RenderRepaintBoundary boundary = ThemeTransitionTarget.of(context)!.boundaryKey.currentContext!.findRenderObject() as RenderRepaintBoundary; var scale = WidgetsBinding.instance.window.devicePixelRatio / 2; var img = await boundary.toImage(pixelRatio: scale); var byteData = await img.toByteData(format: ImageByteFormat.png); - Uint8List pngBytes = byteData.buffer.asUint8List(); + Uint8List pngBytes = byteData!.buffer.asUint8List(); // Custom route to ThemeTransitionOverlay Navigator.of(context).push( @@ -67,8 +67,8 @@ class ThemeTransitionOverlay extends StatefulWidget { class _ThemeTransitionOverlayState extends State with TickerProviderStateMixin { static const maxSplashOpacity = 0.35; - AnimationController _animationController; - Animation _animation; + late AnimationController _animationController; + late Animation _animation; bool _onReadyCalled = false; @override diff --git a/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_target.dart b/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_target.dart index 9df2af68dc..e69f089a51 100644 --- a/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_target.dart +++ b/apps/flutter_parent/lib/utils/design/theme_transition/theme_transition_target.dart @@ -24,9 +24,9 @@ import 'package:flutter_parent/utils/design/theme_transition/theme_transition_ov /// Note that this this widget is intended to be used with a full-screen [child] (e.g. a settings screen) and will not /// work correctly with smaller nested widgets within a screen. class ThemeTransitionTarget extends StatefulWidget { - final Widget child; + final Widget? child; - const ThemeTransitionTarget({Key key, this.child}) : super(key: key); + const ThemeTransitionTarget({this.child, super.key}); @override _ThemeTransitionTargetState createState() => _ThemeTransitionTargetState(); @@ -34,11 +34,11 @@ class ThemeTransitionTarget extends StatefulWidget { /// Toggles dark mode and initiates an animated circular reveal transition to the new theme. [context] must be /// a [BuildContext] that contains a [ThemeTransitionTarget], and [anchorKey] must be a [GlobalKey] assigned /// to a widget from which the animation transition will originate. - static void toggleDarkMode(BuildContext context, GlobalKey anchorKey) { - _toggleMode(context, anchorKey, () => ParentTheme.of(context).toggleDarkMode()); + static void toggleDarkMode(BuildContext context, GlobalKey? anchorKey) { + _toggleMode(context, anchorKey, () => ParentTheme.of(context)?.toggleDarkMode()); } - static void _toggleMode(BuildContext context, GlobalKey anchorKey, Function() toggle) { + static void _toggleMode(BuildContext context, GlobalKey? anchorKey, Function() toggle) { // If testing, just toggle without doing the theme transition overlay if (WidgetsBinding.instance.runtimeType != WidgetsFlutterBinding) { toggle(); @@ -52,7 +52,7 @@ class ThemeTransitionTarget extends StatefulWidget { }); } - static _ThemeTransitionTargetState of(BuildContext context) { + static _ThemeTransitionTargetState? of(BuildContext context) { return context.findAncestorStateOfType<_ThemeTransitionTargetState>(); } } diff --git a/apps/flutter_parent/lib/utils/features_utils.dart b/apps/flutter_parent/lib/utils/features_utils.dart index 3df053af68..8f2a0b9eeb 100644 --- a/apps/flutter_parent/lib/utils/features_utils.dart +++ b/apps/flutter_parent/lib/utils/features_utils.dart @@ -20,7 +20,7 @@ class FeaturesUtils { static const String KEY_SEND_USAGE_METRICS = 'send_usage_metrics'; - static EncryptedSharedPreferences _prefs; + static EncryptedSharedPreferences? _prefs; static Future init() async { if (_prefs == null) _prefs = await EncryptedSharedPreferences.getInstance(); @@ -29,17 +29,17 @@ class FeaturesUtils { static Future checkUsageMetricFeatureFlag() async { await init(); final featureFlags = await locator().getFeatureFlags(); - await _prefs.setBool(KEY_SEND_USAGE_METRICS, featureFlags.sendUsageMetrics); + await _prefs?.setBool(KEY_SEND_USAGE_METRICS, featureFlags?.sendUsageMetrics); } static Future getUsageMetricFeatureFlag() async { await init(); - return await _prefs.getBool(KEY_SEND_USAGE_METRICS) == true; + return await _prefs?.getBool(KEY_SEND_USAGE_METRICS) == true; } static Future performLogout() async { if (_prefs != null) { - await _prefs.remove(KEY_SEND_USAGE_METRICS); + await _prefs?.remove(KEY_SEND_USAGE_METRICS); } } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/utils/notification_util.dart b/apps/flutter_parent/lib/utils/notification_util.dart index c90df25249..5ea59291ad 100644 --- a/apps/flutter_parent/lib/utils/notification_util.dart +++ b/apps/flutter_parent/lib/utils/notification_util.dart @@ -26,19 +26,21 @@ import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/service_locator.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; class NotificationUtil { static const notificationChannelReminders = 'com.instructure.parentapp/reminders'; - static FlutterLocalNotificationsPlugin _plugin; + static FlutterLocalNotificationsPlugin? _plugin; @visibleForTesting static initForTest(FlutterLocalNotificationsPlugin plugin) { _plugin = plugin; } - static Future init(Completer appCompleter) async { + static Future init(Completer? appCompleter) async { var initializationSettings = InitializationSettings( android: AndroidInitializationSettings('ic_notification_canvas_logo') ); @@ -47,22 +49,22 @@ class NotificationUtil { _plugin = FlutterLocalNotificationsPlugin(); } - await _plugin.initialize( + await _plugin!.initialize( initializationSettings, - onSelectNotification: (rawPayload) async { - await handlePayload(rawPayload, appCompleter); + onDidReceiveNotificationResponse: (rawPayload) async { + await handlePayload(rawPayload.payload ?? '', appCompleter); }, ); } @visibleForTesting static Future handlePayload( - String rawPayload, Completer appCompleter) async { + String rawPayload, Completer? appCompleter) async { try { - NotificationPayload payload = deserialize(json.decode(rawPayload)); - switch (payload.type) { + NotificationPayload? payload = deserialize(json.decode(rawPayload)); + switch (payload?.type) { case NotificationPayloadType.reminder: - await handleReminder(payload, appCompleter); + await handleReminder(payload!, appCompleter); break; case NotificationPayloadType.other: break; @@ -74,31 +76,30 @@ class NotificationUtil { @visibleForTesting static Future handleReminder( - NotificationPayload payload, Completer appCompleter) async { - Reminder reminder = Reminder.fromNotification(payload); + NotificationPayload payload, Completer? appCompleter) async { + Reminder? reminder = Reminder.fromNotification(payload); // Delete reminder from db - await locator().deleteById(reminder.id); + await locator().deleteById(reminder?.id); // Create route - String route; - switch (reminder.type) { + String? route; + switch (reminder?.type) { case Reminder.TYPE_ASSIGNMENT: route = - PandaRouter.assignmentDetails(reminder.courseId, reminder.itemId); + PandaRouter.assignmentDetails(reminder!.courseId, reminder.itemId); break; case Reminder.TYPE_EVENT: - route = PandaRouter.eventDetails(reminder.courseId, reminder.itemId); + route = PandaRouter.eventDetails(reminder!.courseId, reminder.itemId); break; } // Push route, but only after the app has finished building - appCompleter.future - .then((_) => WidgetsBinding.instance?.handlePushRoute(route)); + if (route != null) appCompleter?.future.then((_) => WidgetsBinding.instance.handlePushRoute(route!)); } Future scheduleReminder( - AppLocalizations l10n, String title, String body, Reminder reminder) { + AppLocalizations l10n, String? title, String body, Reminder reminder) { final payload = NotificationPayload((b) => b ..type = NotificationPayloadType.reminder ..data = json.encode(serialize(reminder))); @@ -119,19 +120,24 @@ class NotificationUtil { .logEvent(AnalyticsEventConstants.REMINDER_EVENT_CREATE); } - return _plugin.schedule( - reminder.id, + tz.initializeTimeZones(); + var d = reminder.date!.toUtc(); + var date = tz.TZDateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second); + + return _plugin!.zonedSchedule( + reminder.id!, title, body, - reminder.date, + date, notificationDetails, payload: json.encode(serialize(payload)), + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, ); } - Future deleteNotification(int id) => _plugin.cancel(id); + Future deleteNotification(int id) => _plugin!.cancel(id); Future deleteNotifications(List ids) async { - for (int id in ids) await _plugin.cancel(id); + for (int id in ids) await _plugin!.cancel(id); } } diff --git a/apps/flutter_parent/lib/utils/old_app_migration.dart b/apps/flutter_parent/lib/utils/old_app_migration.dart index c489f2f5dc..659eac747e 100644 --- a/apps/flutter_parent/lib/utils/old_app_migration.dart +++ b/apps/flutter_parent/lib/utils/old_app_migration.dart @@ -32,7 +32,7 @@ class OldAppMigration { // Get the list of logins from the native side List data = await channel.invokeMethod(methodGetLogins); - List logins = data.map((it) => deserialize(json.decode(it))).toList(); + List logins = data.map((it) => deserialize(json.decode(it))).toList().nonNulls.toList(); if (logins.isNotEmpty) { // Save the list of logins to prefs diff --git a/apps/flutter_parent/lib/utils/qr_utils.dart b/apps/flutter_parent/lib/utils/qr_utils.dart index 888a823cfb..0b4192246e 100644 --- a/apps/flutter_parent/lib/utils/qr_utils.dart +++ b/apps/flutter_parent/lib/utils/qr_utils.dart @@ -32,10 +32,10 @@ class QRUtils { static const String QR_PAIR_PARAM_CODE = 'code'; static const String QR_PAIR_PARAM_ACCOUNT_ID = 'account_id'; - static Uri verifySSOLogin(String url) { + static Uri? verifySSOLogin(String? url) { + if (url == null) return null; try { var uri = Uri.parse(url); - if (uri == null) return null; var hostList = [QR_HOST, QR_HOST_BETA, QR_HOST_TEST]; if (hostList.contains(uri.host) && uri.queryParameters[QR_DOMAIN] != null && @@ -72,14 +72,15 @@ class QRUtils { } /// Attempts to parse and return QR pairing information from the provided uri. Returns null if parsing failed. - static QRPairingScanResult parsePairingInfo(String rawUri) { + static QRPairingScanResult parsePairingInfo(String? rawUri) { + if (rawUri == null) return QRPairingScanResult.error(QRPairingScanErrorType.invalidCode); try { var uri = Uri.parse(rawUri); var params = uri.queryParameters; if (QR_PAIR_PATH == uri.pathSegments.first && params[QR_PAIR_PARAM_CODE] != null && params[QR_PAIR_PARAM_ACCOUNT_ID] != null) { - return QRPairingScanResult.success(params[QR_PAIR_PARAM_CODE], uri.host, params[QR_PAIR_PARAM_ACCOUNT_ID]); + return QRPairingScanResult.success(params[QR_PAIR_PARAM_CODE]!, uri.host, params[QR_PAIR_PARAM_ACCOUNT_ID]!); } } catch (e) { // Intentionally left blank diff --git a/apps/flutter_parent/lib/utils/quick_nav.dart b/apps/flutter_parent/lib/utils/quick_nav.dart index 96cbb3e540..f26c2e9174 100644 --- a/apps/flutter_parent/lib/utils/quick_nav.dart +++ b/apps/flutter_parent/lib/utils/quick_nav.dart @@ -22,28 +22,28 @@ import 'package:flutter_parent/utils/service_locator.dart'; class QuickNav { @Deprecated('Deprecated in favor of using PushRoute etc, end goal is for all routes to go through PandaRouter') - Future push(BuildContext context, Widget widget) { + Future push(BuildContext context, Widget widget) { _logShow(widget); - return Navigator.of(context).push(MaterialPageRoute(builder: (context) => widget)); + return Navigator.of(context).push(MaterialPageRoute(builder: (context) => widget)); } /// Default method for pushing screens, uses material transition - Future pushRoute(BuildContext context, String route, + Future pushRoute(BuildContext context, String route, {TransitionType transitionType = TransitionType.material}) { return PandaRouter.router.navigateTo(context, route, transition: transitionType); } - Future replaceRoute(BuildContext context, String route, + Future replaceRoute(BuildContext context, String route, {TransitionType transitionType = TransitionType.material}) { return PandaRouter.router.navigateTo(context, route, transition: transitionType, replace: true); } - Future pushRouteAndClearStack(BuildContext context, String route, + Future pushRouteAndClearStack(BuildContext context, String route, {TransitionType transitionType = TransitionType.material}) { return PandaRouter.router.navigateTo(context, route, transition: transitionType, clearStack: true); } - Future pushRouteWithCustomTransition(BuildContext context, String route, bool clearStack, + Future pushRouteWithCustomTransition(BuildContext context, String route, bool clearStack, Duration transitionDuration, RouteTransitionsBuilder transitionsBuilder, {TransitionType transitionType = TransitionType.custom}) { return PandaRouter.router.navigateTo(context, route, @@ -64,12 +64,12 @@ class QuickNav { locator().setCurrentScreen(widgetName); } - Future showDialog({ - @required BuildContext context, + Future showDialog({ + required BuildContext context, bool barrierDismissible = true, - WidgetBuilder builder, + required WidgetBuilder builder, bool useRootNavigator = true, - RouteSettings routeSettings, + RouteSettings? routeSettings, }) => Material.showDialog( context: context, diff --git a/apps/flutter_parent/lib/utils/remote_config_utils.dart b/apps/flutter_parent/lib/utils/remote_config_utils.dart index 9cca68bb29..b944666049 100644 --- a/apps/flutter_parent/lib/utils/remote_config_utils.dart +++ b/apps/flutter_parent/lib/utils/remote_config_utils.dart @@ -23,8 +23,8 @@ enum RemoteConfigParams { } class RemoteConfigUtils { - static RemoteConfig _remoteConfig = null; - static SharedPreferences _prefs; + static FirebaseRemoteConfig? _remoteConfig = null; + static SharedPreferences? _prefs; // I bifurcated initialize() into initialize() and initializeExplicit() to allow for // tests to pass in a mocked RemoteConfig object. @@ -33,7 +33,7 @@ class RemoteConfigUtils { * This is the normal initializer that should be called from production code. **/ static Future initialize() async { - RemoteConfig freshRemoteConfig = await RemoteConfig.instance; + FirebaseRemoteConfig freshRemoteConfig = await FirebaseRemoteConfig.instance; await initializeExplicit(freshRemoteConfig); } @@ -49,18 +49,18 @@ class RemoteConfigUtils { * Only intended for use in test code. Should not be called from production code. */ @visibleForTesting - static Future initializeExplicit(RemoteConfig remoteConfig) async { + static Future initializeExplicit(FirebaseRemoteConfig remoteConfig) async { if (_remoteConfig != null) throw StateError('double-initialization of RemoteConfigUtils'); _remoteConfig = remoteConfig; - _remoteConfig.settings.minimumFetchInterval = Duration(hours: 1); + _remoteConfig!.settings.minimumFetchInterval = Duration(hours: 1); // fetch data from Firebase - var updated = false; + bool updated = false; try { - await _remoteConfig.fetch(); - updated = await _remoteConfig.activate(); + await _remoteConfig?.fetch(); + updated = await _remoteConfig?.activate() ?? false; } catch (e) { // On fetch/activate failure, just make sure that updated is set to false updated = false; @@ -73,12 +73,12 @@ class RemoteConfigUtils { // If we actually fetched something, then store the fetched info into _prefs RemoteConfigParams.values.forEach((rc) { String rcParamName = getRemoteConfigName(rc); - String rcParamValue = _remoteConfig.getString(rcParamName); + String? rcParamValue = _remoteConfig?.getString(rcParamName); String rcPreferencesName = _getSharedPreferencesName(rc); print( 'RemoteConfigUtils.initialize(): fetched $rcParamName=${rcParamValue == null ? 'null' : '\"$rcParamValue\"'}'); if (rcParamValue != null) { - _prefs.setString(rcPreferencesName, rcParamValue); + _prefs?.setString(rcPreferencesName, rcParamValue); } }); } else { @@ -88,7 +88,7 @@ class RemoteConfigUtils { RemoteConfigParams.values.forEach((rc) { String rcParamName = getRemoteConfigName(rc); String rcPreferencesName = _getSharedPreferencesName(rc); - String rcParamValue = _prefs.getString(rcPreferencesName); + String? rcParamValue = _prefs?.getString(rcPreferencesName); print( 'RemoteConfigUtils.initialize(): cached $rcParamName value = ${rcParamValue == null ? 'null' : '\"$rcParamValue\"'}'); }); @@ -102,10 +102,10 @@ class RemoteConfigUtils { var rcDefault = _getRemoteConfigDefaultValue(rcParam); var rcPreferencesName = _getSharedPreferencesName(rcParam); - var result = _prefs.getString(rcPreferencesName); + var result = _prefs?.getString(rcPreferencesName); if (result == null) { result = rcDefault; - _prefs.setString(rcPreferencesName, rcDefault); + _prefs?.setString(rcPreferencesName, rcDefault); } return result; } @@ -150,6 +150,6 @@ class RemoteConfigUtils { } static void updateRemoteConfig(RemoteConfigParams rcParam, String newValue) { - _prefs.setString(_getSharedPreferencesName(rcParam), newValue); + _prefs?.setString(_getSharedPreferencesName(rcParam), newValue); } } diff --git a/apps/flutter_parent/lib/utils/service_locator.dart b/apps/flutter_parent/lib/utils/service_locator.dart index 4165047df5..2866bbe36a 100644 --- a/apps/flutter_parent/lib/utils/service_locator.dart +++ b/apps/flutter_parent/lib/utils/service_locator.dart @@ -72,7 +72,6 @@ import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interac import 'package:flutter_parent/utils/db/calendar_filter_db.dart'; import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/db/user_colors_db.dart'; -import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_parent/utils/old_app_migration.dart'; import 'package:flutter_parent/utils/permission_handler.dart'; @@ -84,7 +83,6 @@ import 'package:flutter_parent/utils/veneers/flutter_downloader_veneer.dart'; import 'package:flutter_parent/utils/veneers/flutter_snackbar_veneer.dart'; import 'package:flutter_parent/utils/veneers/path_provider_veneer.dart'; import 'package:get_it/get_it.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:sqflite/sqflite.dart'; import 'common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart'; diff --git a/apps/flutter_parent/lib/utils/style_slicer.dart b/apps/flutter_parent/lib/utils/style_slicer.dart index 4322a937f6..040b41b21e 100644 --- a/apps/flutter_parent/lib/utils/style_slicer.dart +++ b/apps/flutter_parent/lib/utils/style_slicer.dart @@ -23,8 +23,8 @@ abstract class StyleSlicer { this.recognizer, }); - final TextStyle style; - final GestureRecognizer recognizer; + final TextStyle? style; + final GestureRecognizer? recognizer; List getSlices(String src); @@ -39,7 +39,7 @@ abstract class StyleSlicer { /// later in the list of [slicers] will be used. /// /// A base style for the entire text can be applied by specifying [baseStyle] - static TextSpan apply(String source, List slicers, {TextStyle baseStyle = const TextStyle()}) { + static TextSpan apply(String? source, List? slicers, {TextStyle? baseStyle = const TextStyle()}) { if (source == null || source.isEmpty) return TextSpan(text: ''); if (slicers == null || slicers.isEmpty) return TextSpan(text: source); @@ -49,21 +49,21 @@ abstract class StyleSlicer { Map> opsMap = {}; slicers.forEach((slicer) { - slicer?.getSlices(source)?.forEach((slice) { + slicer.getSlices(source).forEach((slice) { opsMap.putIfAbsent(slice.item1, () => []).add(_SlicerOp(true, slicer)); opsMap.putIfAbsent(slice.item2, () => []).add(_SlicerOp(false, slicer)); }); }); List slicePoints = opsMap.keys.toList()..sort(); - List currentSlicers = []; + List currentSlicers = []; List spans = []; for (int i = 0; i < slicePoints.length - 1; i++) { int start = slicePoints[i]; int end = slicePoints[i + 1]; - opsMap[start].forEach((op) { + opsMap[start]?.forEach((op) { if (op.isAdd) { currentSlicers.add(op.slicer); } else { @@ -73,7 +73,7 @@ abstract class StyleSlicer { String slice = source.substring(start, end); TextStyle style = _mergeStyles(currentSlicers); - var recognizer = currentSlicers.lastWhere((it) => it.recognizer != null, orElse: () => null)?.recognizer; + var recognizer = currentSlicers.lastWhere((it) => it?.recognizer != null, orElse: () => null)?.recognizer; spans.add(TextSpan(text: slice, style: style, recognizer: recognizer)); } @@ -81,9 +81,9 @@ abstract class StyleSlicer { return TextSpan(children: spans); } - static TextStyle _mergeStyles(List currentSlicers) { + static TextStyle _mergeStyles(List currentSlicers) { TextStyle style = TextStyle(); - currentSlicers.forEach((it) => style = style.merge(it.style)); + currentSlicers.forEach((it) => style = style.merge(it?.style)); return style; } } @@ -102,8 +102,8 @@ class RangeSlice extends StyleSlicer { RangeSlice( int start, int end, { - TextStyle style, - GestureRecognizer recognizer, + TextStyle? style, + GestureRecognizer? recognizer, }) : range = Tuple2(start, end), super(style: style, recognizer: recognizer); @@ -116,20 +116,20 @@ class RangeSlice extends StyleSlicer { /// Provides slices that match the given [pattern]. To limit the number of matches, specify a non-negative value /// for [maxMatches]. class PatternSlice extends StyleSlicer { - final Pattern pattern; + final Pattern? pattern; final int maxMatches; PatternSlice( this.pattern, { this.maxMatches = -1, - TextStyle style, - GestureRecognizer recognizer, + TextStyle? style, + GestureRecognizer? recognizer, }) : super(style: style, recognizer: recognizer); @override List getSlices(String src) { if (pattern == null || pattern == '') return []; - var matches = pattern.allMatches(src); + var matches = pattern!.allMatches(src); if (maxMatches > -1) matches = matches.take(maxMatches); return matches.map((it) => Tuple2(it.start, it.end)).toList(); } @@ -139,9 +139,9 @@ class PatternSlice extends StyleSlicer { /// number of matches, specify a non-negative value for [maxMatches]. class PronounSlice extends PatternSlice { PronounSlice( - String pronoun, { + String? pronoun, { int maxMatches = -1, - GestureRecognizer recognizer, + GestureRecognizer? recognizer, }) : super( pronoun == null || pronoun.isEmpty ? null : '($pronoun)', maxMatches: maxMatches, diff --git a/apps/flutter_parent/lib/utils/url_launcher.dart b/apps/flutter_parent/lib/utils/url_launcher.dart index 0725cd14b6..e9088240d0 100644 --- a/apps/flutter_parent/lib/utils/url_launcher.dart +++ b/apps/flutter_parent/lib/utils/url_launcher.dart @@ -21,7 +21,7 @@ class UrlLauncher { MethodChannel channel = MethodChannel(channelName); - Future canLaunch(String url, {bool excludeInstructure = true}) { + Future canLaunch(String url, {bool excludeInstructure = true}) { return channel.invokeMethod( canLaunchMethod, { diff --git a/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart b/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart index 1fdd0c3ced..6e0e191b3e 100644 --- a/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart +++ b/apps/flutter_parent/lib/utils/veneers/flutter_downloader_veneer.dart @@ -16,11 +16,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; class FlutterDownloaderVeneer { - Future enqueue({ - @required String url, - @required String savedDir, - String fileName, - Map headers, + Future enqueue({ + required String url, + required String savedDir, + String? fileName, bool showNotification = true, bool openFileFromNotification = true, bool requiresStorageNotLow = true, @@ -29,39 +28,38 @@ class FlutterDownloaderVeneer { url: url, savedDir: savedDir, fileName: fileName, - headers: headers, showNotification: showNotification, openFileFromNotification: openFileFromNotification, requiresStorageNotLow: requiresStorageNotLow, saveInPublicStorage: saveInPublicStorage); - Future> loadTasks() => FlutterDownloader.loadTasks(); + Future?> loadTasks() => FlutterDownloader.loadTasks(); - static Future> loadTasksWithRawQuery({@required String query}) => + static Future?> loadTasksWithRawQuery({required String query}) => FlutterDownloader.loadTasksWithRawQuery(query: query); - static Future cancel({@required String taskId}) => FlutterDownloader.cancel(taskId: taskId); + static Future cancel({required String taskId}) => FlutterDownloader.cancel(taskId: taskId); - static Future cancelAll() => FlutterDownloader.cancelAll(); + static Future cancelAll() => FlutterDownloader.cancelAll(); - static Future pause({@required String taskId}) => FlutterDownloader.pause(taskId: taskId); + static Future pause({required String taskId}) => FlutterDownloader.pause(taskId: taskId); - static Future resume({ - @required String taskId, + static Future resume({ + required String taskId, bool requiresStorageNotLow = true, }) => FlutterDownloader.resume(taskId: taskId, requiresStorageNotLow: requiresStorageNotLow); - static Future retry({ - @required String taskId, + static Future retry({ + required String taskId, bool requiresStorageNotLow = true, }) => FlutterDownloader.retry(taskId: taskId, requiresStorageNotLow: requiresStorageNotLow); - static Future remove({@required String taskId, bool shouldDeleteContent = false}) => + static Future remove({required String taskId, bool shouldDeleteContent = false}) => FlutterDownloader.remove(taskId: taskId, shouldDeleteContent: shouldDeleteContent); - static Future open({@required String taskId}) => FlutterDownloader.open(taskId: taskId); + static Future open({required String taskId}) => FlutterDownloader.open(taskId: taskId); static registerCallback(DownloadCallback callback) => FlutterDownloader.registerCallback(callback); } diff --git a/apps/flutter_parent/lib/utils/veneers/flutter_snackbar_veneer.dart b/apps/flutter_parent/lib/utils/veneers/flutter_snackbar_veneer.dart index ea4a992d92..de4bc4e48b 100644 --- a/apps/flutter_parent/lib/utils/veneers/flutter_snackbar_veneer.dart +++ b/apps/flutter_parent/lib/utils/veneers/flutter_snackbar_veneer.dart @@ -17,7 +17,7 @@ import 'package:flutter/material.dart'; class FlutterSnackbarVeneer { showSnackBar(context, String message) { - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } diff --git a/apps/flutter_parent/lib/utils/veneers/path_provider_veneer.dart b/apps/flutter_parent/lib/utils/veneers/path_provider_veneer.dart index f9f1133bdd..d70e6f860f 100644 --- a/apps/flutter_parent/lib/utils/veneers/path_provider_veneer.dart +++ b/apps/flutter_parent/lib/utils/veneers/path_provider_veneer.dart @@ -26,11 +26,11 @@ class PathProviderVeneer { Future getApplicationDocumentsDirectory() => PathProvider.getApplicationDocumentsDirectory(); - Future getExternalStorageDirectory() => PathProvider.getExternalStorageDirectory(); + Future getExternalStorageDirectory() => PathProvider.getExternalStorageDirectory(); - Future> getExternalCacheDirectories() => PathProvider.getExternalCacheDirectories(); + Future?> getExternalCacheDirectories() => PathProvider.getExternalCacheDirectories(); - Future> getExternalStorageDirectories({PathProvider.StorageDirectory type}) { + Future?> getExternalStorageDirectories({PathProvider.StorageDirectory? type}) { return PathProvider.getExternalStorageDirectories(type: type); } } diff --git a/apps/flutter_parent/lib/utils/web_view_utils.dart b/apps/flutter_parent/lib/utils/web_view_utils.dart index 1a8bfe49a6..cd46d97c27 100644 --- a/apps/flutter_parent/lib/utils/web_view_utils.dart +++ b/apps/flutter_parent/lib/utils/web_view_utils.dart @@ -26,18 +26,20 @@ extension WebViewUtils on WebViewController { * * See html_wrapper.html for more details */ - Future loadHtml(String html, - {String baseUrl, Map headers, double horizontalPadding = 0}) async { - assert(horizontalPadding != null); - - String fileText = await rootBundle.loadString('assets/html/html_wrapper.html'); - html = _applyWorkAroundForDoubleSlashesAsUrlSource(html); - html = _addProtocolToLinks(html); - html = _checkForMathTags(html); - html = fileText.replaceAll('{CANVAS_CONTENT}', html); - html = html.replaceAll('{PADDING}', horizontalPadding.toString()); - this.loadData(baseUrl, html, 'text/html', 'utf-8'); - } + Future loadHtml( + String? html, { + String? baseUrl, + Map? headers, + double horizontalPadding = 0}) + async { + String fileText = await rootBundle.loadString('assets/html/html_wrapper.html'); + html = _applyWorkAroundForDoubleSlashesAsUrlSource(html); + html = _addProtocolToLinks(html); + html = _checkForMathTags(html); + html = fileText.replaceAll('{CANVAS_CONTENT}', html); + html = html.replaceAll('{PADDING}', horizontalPadding.toString()); + this.loadData(baseUrl, html, 'text/html', 'utf-8'); + } /** * Loads html content w/o any change to formatting @@ -58,8 +60,8 @@ String _checkForMathTags(String html) { } } -String _applyWorkAroundForDoubleSlashesAsUrlSource(String html) { - if (html.isEmpty) return ''; +String _applyWorkAroundForDoubleSlashesAsUrlSource(String? html) { + if (html == null || html.isEmpty) return ''; // Fix for embedded videos that have // instead of http:// html = html.replaceAll('href="//', 'href="https://'); html = html.replaceAll('href=\'//', 'href=\'https://'); @@ -89,7 +91,7 @@ String _addProtocolToLinks(String html) { /// https://github.com/flutter/flutter/issues/36304 /// https://github.com/flutter/flutter/issues/35394 class WebViewGestureRecognizer extends VerticalDragGestureRecognizer { - WebViewGestureRecognizer({PointerDeviceKind kind}) : super(kind: kind); + WebViewGestureRecognizer(); @override get onUpdate => (_) {}; diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle b/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle index 500b99b3d0..0551809ceb 100644 --- a/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle +++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath "com.android.tools.build:gradle:7.4.2" } } @@ -30,7 +30,7 @@ allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 33 defaultConfig { minSdkVersion 23 @@ -39,6 +39,8 @@ android { lintOptions { disable 'InvalidPackage' } + + namespace 'com.instructure.parentapp.encryptedsharedpreferences' } dependencies { diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/encrypted_shared_preferences.dart b/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/encrypted_shared_preferences.dart index 14465cb306..d26adff96f 100644 --- a/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/encrypted_shared_preferences.dart +++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/encrypted_shared_preferences.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:meta/meta.dart'; import 'shared_preferences_platform_interface.dart'; @@ -15,7 +16,7 @@ class EncryptedSharedPreferences { EncryptedSharedPreferences._(this._preferenceCache); static const String _prefix = 'flutter.'; - static Completer _completer; + static Completer? _completer; static EncryptedSharedPreferencesStorePlatform get _store => EncryptedSharedPreferencesStorePlatform.instance; @@ -28,17 +29,17 @@ class EncryptedSharedPreferences { _completer = Completer(); try { final Map preferencesMap = await _getSharedPreferencesMap(); - _completer.complete(EncryptedSharedPreferences._(preferencesMap)); + _completer!.complete(EncryptedSharedPreferences._(preferencesMap)); } on Exception catch (e) { // If there's an error, explicitly return the future with an error. // then set the completer to null so we can retry. - _completer.completeError(e); - final Future sharedPrefsFuture = _completer.future; + _completer!.completeError(e); + final Future sharedPrefsFuture = _completer!.future; _completer = null; return sharedPrefsFuture; } } - return _completer.future; + return _completer!.future; } /// The cache that holds all preferences. @@ -48,7 +49,7 @@ class EncryptedSharedPreferences { /// /// It is NOT guaranteed that this cache and the device prefs will remain /// in sync since the setter method might fail for any reason. - final Map _preferenceCache; + final Map _preferenceCache; /// Returns all keys in the persistent storage. Set getKeys() => Set.from(_preferenceCache.keys); @@ -58,19 +59,19 @@ class EncryptedSharedPreferences { /// Reads a value from persistent storage, throwing an exception if it's not a /// bool. - bool getBool(String key) => _preferenceCache[key]; + bool? getBool(String key) => _preferenceCache[key] as bool?; /// Reads a value from persistent storage, throwing an exception if it's not /// an int. - int getInt(String key) => _preferenceCache[key]; + int? getInt(String key) => _preferenceCache[key] as int?; /// Reads a value from persistent storage, throwing an exception if it's not a /// double. - double getDouble(String key) => _preferenceCache[key]; + double? getDouble(String key) => _preferenceCache[key] as double?; /// Reads a value from persistent storage, throwing an exception if it's not a /// String. - String getString(String key) => _preferenceCache[key]; + String? getString(String key) => _preferenceCache[key] as String?; /// Returns true if persistent storage the contains the given [key]. bool containsKey(String key) => _preferenceCache.containsKey(key); @@ -78,46 +79,48 @@ class EncryptedSharedPreferences { /// Reads a set of string values from persistent storage, throwing an /// exception if it's not a string set. List getStringList(String key) { - List list = _preferenceCache[key]; - if (list != null && list is! List) { - list = list.cast().toList(); - _preferenceCache[key] = list; + dynamic list = _preferenceCache[key]; + List? castedList; + try { + castedList = (list as List).map((e) => e as String).toList(); + } catch (e) { + castedList = []; } // Make a copy of the list so that later mutations won't propagate - return list?.toList(); + return castedList; } /// Saves a boolean [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setBool(String key, bool value) => _setValue('Bool', key, value); + Future setBool(String key, bool? value) => _setValue('Bool', key, value); /// Saves an integer [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setInt(String key, int value) => _setValue('Int', key, value); + Future setInt(String key, int? value) => _setValue('Int', key, value); /// Saves a double [value] to persistent storage in the background. /// /// Android doesn't support storing doubles, so it will be stored as a float. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setDouble(String key, double value) => _setValue('Double', key, value); + Future setDouble(String key, double? value) => _setValue('Double', key, value); /// Saves a string [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setString(String key, String value) => _setValue('String', key, value); + Future setString(String key, String? value) => _setValue('String', key, value); /// Saves a list of strings [value] to persistent storage in the background. /// /// If [value] is null, this is equivalent to calling [remove()] on the [key]. - Future setStringList(String key, List value) => _setValue('StringList', key, value); + Future setStringList(String key, List? value) => _setValue('StringList', key, value); /// Removes an entry from persistent storage. Future remove(String key) => _setValue(null, key, null); - Future _setValue(String valueType, String key, Object value) { + Future _setValue(String? valueType, String key, Object? value) { final String prefixedKey = '$_prefix$key'; if (value == null) { _preferenceCache.remove(key); @@ -125,11 +128,11 @@ class EncryptedSharedPreferences { } else { if (value is List) { // Make a copy of the list so that later mutations won't propagate - _preferenceCache[key] = value.toList(); + _preferenceCache[key] = [...value.toList()]; } else { _preferenceCache[key] = value; } - return _store.setValue(valueType, prefixedKey, value); + return _store.setValue(valueType!, prefixedKey, value); } } @@ -139,7 +142,7 @@ class EncryptedSharedPreferences { Future commit() async => true; /// Completes with true once the user preferences for the app has been cleared. - Future clear() { + Future clear() async { _preferenceCache.clear(); return _store.clear(); } @@ -156,12 +159,11 @@ class EncryptedSharedPreferences { static Future> _getSharedPreferencesMap() async { final Map fromSystem = await _store.getAll(); - assert(fromSystem != null); // Strip the flutter. prefix from the returned preferences. final Map preferencesMap = {}; for (String key in fromSystem.keys) { assert(key.startsWith(_prefix)); - preferencesMap[key.substring(_prefix.length)] = fromSystem[key]; + preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; } return preferencesMap; } @@ -170,13 +172,13 @@ class EncryptedSharedPreferences { /// /// If the singleton instance has been initialized already, it is nullified. @visibleForTesting - static void setMockInitialValues(Map values) { - final Map newValues = values.map((String key, dynamic value) { + static void setMockInitialValues(Map values) { + final Map newValues = values.map((String key, Object value) { String newKey = key; if (!key.startsWith(_prefix)) { newKey = '$_prefix$key'; } - return MapEntry(newKey, value); + return MapEntry(newKey, value); }); EncryptedSharedPreferencesStorePlatform.instance = InMemoryEncryptedSharedPreferencesStore.withData(newValues); _completer = null; diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/method_channel_shared_preferences.dart b/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/method_channel_shared_preferences.dart index 1d93cba972..7564190358 100644 --- a/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/method_channel_shared_preferences.dart +++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/lib/method_channel_shared_preferences.dart @@ -42,12 +42,16 @@ class MethodChannelEncryptedSharedPreferencesStore extends EncryptedSharedPrefer } @override - Future clear() { - return _kChannel.invokeMethod('clear'); + Future clear() async { + bool? result = await _kChannel.invokeMethod('clear'); + if (result == null) return false; + return result; } @override - Future> getAll() { - return _kChannel.invokeMapMethod('getAll'); + Future> getAll() async { + Map? results = await _kChannel.invokeMapMethod('getAll'); + if (results == null) return {}; + return results; } } diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/pubspec.yaml b/apps/flutter_parent/plugins/encrypted_shared_preferences/pubspec.yaml index 5c6465b55a..17508e3735 100644 --- a/apps/flutter_parent/plugins/encrypted_shared_preferences/pubspec.yaml +++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/pubspec.yaml @@ -26,5 +26,5 @@ dev_dependencies: pedantic: ^1.8.0 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: "2.5.3" + sdk: ">=3.0.0 <3.10.6" + flutter: 3.13.2 diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/test/method_channel_shared_preferences_test.dart b/apps/flutter_parent/plugins/encrypted_shared_preferences/test/method_channel_shared_preferences_test.dart index 1c4d627ad6..a391e63704 100644 --- a/apps/flutter_parent/plugins/encrypted_shared_preferences/test/method_channel_shared_preferences_test.dart +++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/test/method_channel_shared_preferences_test.dart @@ -15,7 +15,7 @@ void main() { 'com.instructure.parentapp/encrypted_shared_preferences', ); - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.Bool': true, 'flutter.Int': 42, @@ -23,10 +23,10 @@ void main() { 'flutter.StringList': ['foo', 'bar'], }; - InMemoryEncryptedSharedPreferencesStore testData; + InMemoryEncryptedSharedPreferencesStore? testData; final List log = []; - MethodChannelEncryptedSharedPreferencesStore store; + MethodChannelEncryptedSharedPreferencesStore? store; setUp(() async { testData = InMemoryEncryptedSharedPreferencesStore.empty(); @@ -34,22 +34,22 @@ void main() { channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); if (methodCall.method == 'getAll') { - return await testData.getAll(); + return await testData?.getAll(); } if (methodCall.method == 'remove') { final String key = methodCall.arguments['key']; - return await testData.remove(key); + return await testData?.remove(key); } if (methodCall.method == 'clear') { - return await testData.clear(); + return await testData?.clear(); } final RegExp setterRegExp = RegExp(r'set(.*)'); - final Match match = setterRegExp.matchAsPrefix(methodCall.method); - if (match.groupCount == 1) { - final String valueType = match.group(1); + final Match? match = setterRegExp.matchAsPrefix(methodCall.method); + if (match != null && match.groupCount == 1) { + final String valueType = match.group(1) ?? ''; final String key = methodCall.arguments['key']; final Object value = methodCall.arguments['value']; - return await testData.setValue(valueType, key, value); + return await testData?.setValue(valueType, key, value); } fail('Unexpected method call: ${methodCall.method}'); }); @@ -58,24 +58,24 @@ void main() { }); tearDown(() async { - await testData.clear(); + await testData?.clear(); store = null; testData = null; }); test('getAll', () async { testData = InMemoryEncryptedSharedPreferencesStore.withData(kTestValues); - expect(await store.getAll(), kTestValues); + expect(await store?.getAll(), kTestValues); expect(log.single.method, 'getAll'); }); test('remove', () async { testData = InMemoryEncryptedSharedPreferencesStore.withData(kTestValues); - expect(await store.remove('flutter.String'), true); - expect(await store.remove('flutter.Bool'), true); - expect(await store.remove('flutter.Int'), true); - expect(await store.remove('flutter.Double'), true); - expect(await testData.getAll(), { + expect(await store?.remove('flutter.String'), true); + expect(await store?.remove('flutter.Bool'), true); + expect(await store?.remove('flutter.Int'), true); + expect(await store?.remove('flutter.Double'), true); + expect(await testData?.getAll(), { 'flutter.StringList': ['foo', 'bar'], }); @@ -86,12 +86,12 @@ void main() { }); test('setValue', () async { - expect(await testData.getAll(), isEmpty); + expect(await testData?.getAll(), isEmpty); for (String key in kTestValues.keys) { final dynamic value = kTestValues[key]; - expect(await store.setValue(key.split('.').last, key, value), true); + expect(await store?.setValue(key.split('.').last, key, value), true); } - expect(await testData.getAll(), kTestValues); + expect(await testData?.getAll(), kTestValues); expect(log, hasLength(5)); expect(log[0].method, 'setString'); @@ -103,9 +103,9 @@ void main() { test('clear', () async { testData = InMemoryEncryptedSharedPreferencesStore.withData(kTestValues); - expect(await testData.getAll(), isNotEmpty); - expect(await store.clear(), true); - expect(await testData.getAll(), isEmpty); + expect(await testData?.getAll(), isNotEmpty); + expect(await store?.clear(), true); + expect(await testData?.getAll(), isEmpty); expect(log.single.method, 'clear'); }); }); diff --git a/apps/flutter_parent/plugins/encrypted_shared_preferences/test/shared_preferences_test.dart b/apps/flutter_parent/plugins/encrypted_shared_preferences/test/shared_preferences_test.dart index 9e61c8c879..bff56b5835 100755 --- a/apps/flutter_parent/plugins/encrypted_shared_preferences/test/shared_preferences_test.dart +++ b/apps/flutter_parent/plugins/encrypted_shared_preferences/test/shared_preferences_test.dart @@ -11,7 +11,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferences', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -19,7 +19,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -27,8 +27,8 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - FakeSharedPreferencesStore store; - EncryptedSharedPreferences preferences; + late FakeSharedPreferencesStore store; + late EncryptedSharedPreferences preferences; setUp(() async { store = FakeSharedPreferencesStore(kTestValues); @@ -57,11 +57,11 @@ void main() { test('writing', () async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setString('String', kTestValues2['flutter.String'] as String), + preferences.setBool('bool', kTestValues2['flutter.bool'] as bool), + preferences.setInt('int', kTestValues2['flutter.int'] as int), + preferences.setDouble('double', kTestValues2['flutter.double'] as double), + preferences.setStringList('List', kTestValues2['flutter.List'] as List) ]); expect( store.log, @@ -143,7 +143,7 @@ void main() { }); test('reloading', () async { - await preferences.setString('String', kTestValues['flutter.String']); + await preferences.setString('String', kTestValues['flutter.String'] as String); expect(preferences.getString('String'), kTestValues['flutter.String']); EncryptedSharedPreferences.setMockInitialValues(kTestValues2); @@ -164,16 +164,16 @@ void main() { const String _prefixedKey = 'flutter.' + _key; test('test 1', () async { - EncryptedSharedPreferences.setMockInitialValues({_prefixedKey: 'my string'}); + EncryptedSharedPreferences.setMockInitialValues({_prefixedKey: 'my string'}); final EncryptedSharedPreferences prefs = await EncryptedSharedPreferences.getInstance(); - final String value = prefs.getString(_key); + final String? value = prefs.getString(_key); expect(value, 'my string'); }); test('test 2', () async { - EncryptedSharedPreferences.setMockInitialValues({_prefixedKey: 'my other string'}); + EncryptedSharedPreferences.setMockInitialValues({_prefixedKey: 'my other string'}); final EncryptedSharedPreferences prefs = await EncryptedSharedPreferences.getInstance(); - final String value = prefs.getString(_key); + final String? value = prefs.getString(_key); expect(value, 'my other string'); }); }); @@ -197,7 +197,7 @@ void main() { 'test': 'foo', }); final EncryptedSharedPreferences prefs = await EncryptedSharedPreferences.getInstance(); - final String value = prefs.getString('test'); + final String? value = prefs.getString('test'); expect(value, 'foo'); }); } diff --git a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle b/apps/flutter_parent/plugins/webview_flutter/android/build.gradle index 847cb4077c..9e807667a5 100644 --- a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle +++ b/apps/flutter_parent/plugins/webview_flutter/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath "com.android.tools.build:gradle:7.4.2" } } @@ -22,10 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 33 defaultConfig { - minSdkVersion 16 + minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -34,6 +34,8 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.webkit:webkit:1.2.0' + implementation 'androidx.webkit:webkit:1.7.0' } + + namespace 'io.flutter.plugins.webviewflutter' } diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index c5f105b70e..cd0e518ea6 100644 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -425,7 +425,9 @@ private void applySettings(Map settings) { } private void setDarkMode(boolean darkMode) { - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(webView.getSettings(), darkMode); + } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { int forceDarkMode = darkMode ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF; WebSettingsCompat.setForceDark(webView.getSettings(), forceDarkMode); } else { diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart b/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart index 91e964f8dd..2637e0d9f0 100644 --- a/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart +++ b/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart @@ -103,13 +103,12 @@ class WebResourceError { /// A user should not need to instantiate this class, but will receive one in /// [WebResourceErrorCallback]. WebResourceError({ - @required this.errorCode, - @required this.description, + required this.errorCode, + required this.description, this.domain, this.errorType, this.failingUrl, - }) : assert(errorCode != null), - assert(description != null); + }) {} /// Raw code of the error from the respective platform. /// @@ -131,7 +130,7 @@ class WebResourceError { /// in Objective-C. See /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html /// for more information on error handling on iOS. - final String domain; + final String? domain; /// Description of the error that can be used to communicate the problem to the user. final String description; @@ -139,13 +138,13 @@ class WebResourceError { /// The type this error can be categorized as. /// /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType errorType; + final WebResourceErrorType? errorType; /// Gets the URL for which the resource request was made. /// /// This value is not provided on iOS. Alternatively, you can keep track of /// the last values provided to [WebViewPlatformController.loadUrl]. - final String failingUrl; + final String? failingUrl; } /// Interface for talking to the webview's platform implementation. @@ -176,14 +175,14 @@ abstract class WebViewPlatformController { /// Throws an ArgumentError if `url` is not a valid URL string. Future loadUrl( String url, - Map headers, + Map? headers, ) { throw UnimplementedError( "WebView loadUrl is not implemented on the current platform"); } Future loadData( - String baseUrl, + String? baseUrl, String data, String mimeType, String encoding @@ -212,19 +211,19 @@ abstract class WebViewPlatformController { /// Accessor to the current URL that the WebView is displaying. /// /// If no URL was ever loaded, returns `null`. - Future currentUrl() { + Future currentUrl() { throw UnimplementedError( "WebView currentUrl is not implemented on the current platform"); } /// Checks whether there's a back history item. - Future canGoBack() { + Future canGoBack() { throw UnimplementedError( "WebView canGoBack is not implemented on the current platform"); } /// Checks whether there's a forward history item. - Future canGoForward() { + Future canGoForward() { throw UnimplementedError( "WebView canGoForward is not implemented on the current platform"); } @@ -268,7 +267,7 @@ abstract class WebViewPlatformController { /// /// The Future completes with an error if a JavaScript error occurred, or if the type of the /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { + Future evaluateJavascript(String javascriptString) { throw UnimplementedError( "WebView evaluateJavascript is not implemented on the current platform"); } @@ -299,7 +298,7 @@ abstract class WebViewPlatformController { } /// Returns the title of the currently loaded page. - Future getTitle() { + Future getTitle() { throw UnimplementedError( "WebView getTitle is not implemented on the current platform"); } @@ -323,7 +322,7 @@ abstract class WebViewPlatformController { /// Return the horizontal scroll position of this view. /// /// Scroll position is measured from left. - Future getScrollX() { + Future getScrollX() { throw UnimplementedError( "WebView getScrollX is not implemented on the current platform"); } @@ -331,7 +330,7 @@ abstract class WebViewPlatformController { /// Return the vertical scroll position of this view. /// /// Scroll position is measured from top. - Future getScrollY() { + Future getScrollY() { throw UnimplementedError( "WebView getScrollY is not implemented on the current platform"); } @@ -355,7 +354,7 @@ class WebSetting { : _value = value, isPresent = true; - final T _value; + final T? _value; /// The setting's value. /// @@ -364,8 +363,11 @@ class WebSetting { if (!isPresent) { throw StateError('Cannot access a value of an absent WebSetting'); } + if (_value == null) { + throw StateError('Cannot access a value of an absent WebSetting'); + } assert(isPresent); - return _value; + return _value!; } /// True when this web setting instance contains a value. @@ -374,8 +376,9 @@ class WebSetting { final bool isPresent; @override - bool operator ==(Object other) { + bool operator ==(other) { if (other.runtimeType != runtimeType) return false; + if (other is! WebSetting) return false; final WebSetting typedOther = other; return typedOther.isPresent == isPresent && typedOther._value == _value; } @@ -401,21 +404,21 @@ class WebSettings { this.darkMode, this.debuggingEnabled, this.gestureNavigationEnabled, - @required this.userAgent, - }) : assert(userAgent != null); + required this.userAgent, + }) {} /// The JavaScript execution mode to be used by the webview. - final JavascriptMode javascriptMode; + final JavascriptMode? javascriptMode; /// Whether the [WebView] has a [NavigationDelegate] set. - final bool hasNavigationDelegate; + final bool? hasNavigationDelegate; - final bool darkMode; + final bool? darkMode; /// Whether to enable the platform's webview content debugging tools. /// /// See also: [WebView.debuggingEnabled]. - final bool debuggingEnabled; + final bool? debuggingEnabled; /// The value used for the HTTP `User-Agent:` request header. /// @@ -430,7 +433,7 @@ class WebSettings { /// Whether to allow swipe based navigation in iOS. /// /// See also: [WebView.gestureNavigationEnabled] - final bool gestureNavigationEnabled; + final bool? gestureNavigationEnabled; @override String toString() { @@ -453,17 +456,17 @@ class CreationParams { this.userAgent, this.autoMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : assert(autoMediaPlaybackPolicy != null); + }) {} /// The initialUrl to load in the webview. /// /// When null the webview will be created without loading any page. - final String initialUrl; + final String? initialUrl; /// The initial [WebSettings] for the new webview. /// /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings webSettings; + final WebSettings? webSettings; /// The initial set of JavaScript channels that are configured for this webview. /// @@ -476,12 +479,12 @@ class CreationParams { /// ``` // TODO(amirh): describe what should happen when postMessage is called once that code is migrated // to PlatformWebView. - final Set javascriptChannelNames; + final Set? javascriptChannelNames; /// The value used for the HTTP User-Agent: request header. /// /// When null the platform's webview default is used for the User-Agent header. - final String userAgent; + final String? userAgent; /// Which restrictions apply on automatic media playback. final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; @@ -531,9 +534,9 @@ abstract class WebViewPlatform { // I'm starting without it as the PR is starting to become pretty big. // I'll followup with the conversion PR. CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + Set>? gestureRecognizers, }); /// Clears all cookies for all [WebView] instances. diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart index f7afcc0637..f987f5b67f 100644 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart +++ b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart @@ -20,11 +20,11 @@ import 'webview_method_channel.dart'; class AndroidWebView implements WebViewPlatform { @override Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + BuildContext? context, + CreationParams? creationParams, + @required WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, }) { assert(webViewPlatformCallbacksHandler != null); return GestureDetector( @@ -40,7 +40,7 @@ class AndroidWebView implements WebViewPlatform { child: AndroidView( viewType: 'plugins.flutter.io/webview', onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { + if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { return; } onWebViewPlatformCreated(MethodChannelWebViewPlatform( @@ -52,7 +52,7 @@ class AndroidWebView implements WebViewPlatform { // directionality. layoutDirection: TextDirection.rtl, creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, creationParamsCodec: const StandardMessageCodec(), ), ); diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart index 0e84908261..52e7b823fc 100644 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart +++ b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart @@ -20,16 +20,16 @@ import 'webview_method_channel.dart'; class CupertinoWebView implements WebViewPlatform { @override Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + BuildContext? context, + CreationParams? creationParams, + @required WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, }) { return UiKitView( viewType: 'plugins.flutter.io/webview', onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { + if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { return; } onWebViewPlatformCreated( @@ -37,7 +37,7 @@ class CupertinoWebView implements WebViewPlatform { }, gestureRecognizers: gestureRecognizers, creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, creationParamsCodec: const StandardMessageCodec(), ); } diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart index 7d26e60909..7d7e232ff8 100644 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart +++ b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart @@ -13,8 +13,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { /// Constructs an instance that will listen for webviews broadcasting to the /// given [id], using the given [WebViewPlatformCallbacksHandler]. MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) - : assert(_platformCallbacksHandler != null), - _channel = MethodChannel('plugins.flutter.io/webview_$id') { + : _channel = MethodChannel('plugins.flutter.io/webview_$id') { _channel.setMethodCallHandler(_onMethodCall); } @@ -25,7 +24,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { static const MethodChannel _cookieManagerChannel = MethodChannel('plugins.flutter.io/cookie_manager'); - Future _onMethodCall(MethodCall call) async { + Future _onMethodCall(MethodCall call) async { switch (call.method) { case 'javascriptChannelMessage': final String channel = call.arguments['channel']; @@ -51,7 +50,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { domain: call.arguments['domain'], failingUrl: call.arguments['failingUrl'], errorType: call.arguments['errorType'] == null - ? null + ? WebResourceErrorType.unknown : WebResourceErrorType.values.firstWhere( (WebResourceErrorType type) { return type.toString() == @@ -71,9 +70,8 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { @override Future loadUrl( String url, - Map headers, + Map? headers, ) async { - assert(url != null); return _channel.invokeMethod('loadUrl', { 'url': url, 'headers': headers, @@ -81,7 +79,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future loadData(String baseUrl, String data, String mimeType, String encoding) async { + Future loadData(String? baseUrl, String data, String mimeType, String encoding) async { return _channel.invokeMethod('loadData', { 'baseUrl': baseUrl, 'data': data, @@ -107,13 +105,13 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); + Future currentUrl() => _channel.invokeMethod('currentUrl'); @override - Future canGoBack() => _channel.invokeMethod("canGoBack"); + Future canGoBack() => _channel.invokeMethod("canGoBack"); @override - Future canGoForward() => _channel.invokeMethod("canGoForward"); + Future canGoForward() => _channel.invokeMethod("canGoForward"); @override Future goBack() => _channel.invokeMethod("goBack"); @@ -131,15 +129,14 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { Future updateSettings(WebSettings settings) { final Map updatesMap = _webSettingsToMap(settings); if (updatesMap.isEmpty) { - return null; + return Future.value(); } return _channel.invokeMethod('updateSettings', updatesMap); } @override - Future evaluateJavascript(String javascriptString) { - return _channel.invokeMethod( - 'evaluateJavascript', javascriptString); + Future evaluateJavascript(String javascriptString) { + return _channel.invokeMethod('evaluateJavascript', javascriptString); } @override @@ -155,7 +152,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future getTitle() => _channel.invokeMethod("getTitle"); + Future getTitle() => _channel.invokeMethod("getTitle"); @override Future scrollTo(int x, int y) { @@ -174,10 +171,10 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future getScrollX() => _channel.invokeMethod("getScrollX"); + Future getScrollX() => _channel.invokeMethod("getScrollX"); @override - Future getScrollY() => _channel.invokeMethod("getScrollY"); + Future getScrollY() => _channel.invokeMethod("getScrollY"); /// Method channel implementation for [WebViewPlatform.clearCookies]. static Future clearCookies() { @@ -186,7 +183,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { .then((dynamic result) => result); } - static Map _webSettingsToMap(WebSettings settings) { + static Map _webSettingsToMap(WebSettings? settings) { final Map map = {}; void _addIfNonNull(String key, dynamic value) { if (value == null) { @@ -202,13 +199,13 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { map[key] = setting.value; } - _addIfNonNull('jsMode', settings.javascriptMode?.index); - _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); - _addIfNonNull('darkMode', settings.darkMode); - _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + _addIfNonNull('jsMode', settings?.javascriptMode?.index); + _addIfNonNull('hasNavigationDelegate', settings?.hasNavigationDelegate); + _addIfNonNull('darkMode', settings?.darkMode); + _addIfNonNull('debuggingEnabled', settings?.debuggingEnabled); _addIfNonNull( - 'gestureNavigationEnabled', settings.gestureNavigationEnabled); - _addSettingIfPresent('userAgent', settings.userAgent); + 'gestureNavigationEnabled', settings?.gestureNavigationEnabled); + _addSettingIfPresent('userAgent', settings?.userAgent as WebSetting); return map; } @@ -221,7 +218,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { return { 'initialUrl': creationParams.initialUrl, 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), + 'javascriptChannelNames': creationParams.javascriptChannelNames?.toList(), 'userAgent': creationParams.userAgent, 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, }; diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart b/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart index 96dfd72a2c..3a846f6c22 100644 --- a/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart +++ b/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart @@ -33,7 +33,7 @@ class JavascriptMessage { /// Constructs a JavaScript message object. /// /// The `message` parameter must not be null. - const JavascriptMessage(this.message) : assert(message != null); + const JavascriptMessage(this.message); /// The contents of the message that was sent by the JavaScript code. final String message; @@ -44,7 +44,7 @@ typedef void JavascriptMessageHandler(JavascriptMessage message); /// Information about a navigation action that is about to be executed. class NavigationRequest { - NavigationRequest._({this.url, this.isForMainFrame}); + NavigationRequest._({ required this.url, required this.isForMainFrame}); /// The URL that will be loaded if the navigation is executed. final String url; @@ -79,11 +79,11 @@ enum NavigationDecision { class SurfaceAndroidWebView extends AndroidWebView { @override Widget build({ - BuildContext context, - CreationParams creationParams, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + BuildContext? context, + CreationParams? creationParams, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, }) { assert(webViewPlatformCallbacksHandler != null); return PlatformViewLink( @@ -93,7 +93,7 @@ class SurfaceAndroidWebView extends AndroidWebView { PlatformViewController controller, ) { return AndroidViewSurface( - controller: controller, + controller: controller as AndroidViewController, gestureRecognizers: gestureRecognizers ?? const >{}, hitTestBehavior: PlatformViewHitTestBehavior.opaque, @@ -107,14 +107,12 @@ class SurfaceAndroidWebView extends AndroidWebView { // we explicitly set it here so that the widget doesn't require an ambient // directionality. layoutDirection: TextDirection.rtl, - creationParams: MethodChannelWebViewPlatform.creationParamsToMap( - creationParams, - ), + creationParams: creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, creationParamsCodec: const StandardMessageCodec(), ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null) { + if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { return; } onWebViewPlatformCreated( @@ -172,11 +170,9 @@ class JavascriptChannel { /// /// The parameters `name` and `onMessageReceived` must not be null. JavascriptChannel({ - @required this.name, - @required this.onMessageReceived, - }) : assert(name != null), - assert(onMessageReceived != null), - assert(_validChannelNames.hasMatch(name)); + required this.name, + required this.onMessageReceived, + }) : assert(_validChannelNames.hasMatch(name)); /// The channel's name. /// @@ -208,7 +204,7 @@ class WebView extends StatefulWidget { /// /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. const WebView({ - Key key, + Key? key, this.onWebViewCreated, this.initialUrl, this.javascriptMode = JavascriptMode.disabled, @@ -224,11 +220,9 @@ class WebView extends StatefulWidget { this.userAgent, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - super(key: key); + }) : super(key: key); - static WebViewPlatform _platform; + static WebViewPlatform? _platform; /// Sets a custom [WebViewPlatform]. /// @@ -258,11 +252,11 @@ class WebView extends StatefulWidget { "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); } } - return _platform; + return _platform!; } /// If not null invoked once the web view is created. - final WebViewCreatedCallback onWebViewCreated; + final WebViewCreatedCallback? onWebViewCreated; /// Which gestures should be consumed by the web view. /// @@ -273,10 +267,10 @@ class WebView extends StatefulWidget { /// /// When this set is empty or null, the web view will only handle pointer events for gestures that /// were not claimed by any other gesture recognizer. - final Set> gestureRecognizers; + final Set>? gestureRecognizers; /// The initial URL to load. - final String initialUrl; + final String? initialUrl; /// Whether Javascript execution is enabled. final JavascriptMode javascriptMode; @@ -308,7 +302,7 @@ class WebView extends StatefulWidget { /// channels in the list. /// /// A null value is equivalent to an empty set. - final Set javascriptChannels; + final Set? javascriptChannels; /// A delegate function that decides how to handle navigation actions. /// @@ -332,10 +326,10 @@ class WebView extends StatefulWidget { /// * When a navigationDelegate is set pages with frames are not properly handled by the /// webview, and frames will be opened in the main frame. /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate navigationDelegate; + final NavigationDelegate? navigationDelegate; /// Invoked when a page starts loading. - final PageStartedCallback onPageStarted; + final PageStartedCallback? onPageStarted; /// Invoked when a page has finished loading. /// @@ -347,15 +341,15 @@ class WebView extends StatefulWidget { /// When invoked on iOS or Android, any Javascript code that is embedded /// directly in the HTML has been loaded and code injected with /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback onPageFinished; + final PageFinishedCallback? onPageFinished; /// Invoked when a web resource has failed to load. /// /// This can be called for any resource (iframe, image, etc.), not just for /// the main page. - final WebResourceErrorCallback onWebResourceError; + final WebResourceErrorCallback? onWebResourceError; - final bool darkMode; + final bool? darkMode; /// Controls whether WebView debugging is enabled. /// @@ -389,7 +383,7 @@ class WebView extends StatefulWidget { /// user agent. /// /// By default `userAgent` is null. - final String userAgent; + final String? userAgent; /// Which restrictions apply on automatic media playback. /// @@ -407,7 +401,7 @@ class _WebViewState extends State { final Completer _controller = Completer(); - _PlatformCallbacksHandler _platformCallbacksHandler; + late _PlatformCallbacksHandler _platformCallbacksHandler; @override Widget build(BuildContext context) { @@ -442,17 +436,17 @@ class _WebViewState extends State { WebViewController._(widget, webViewPlatform, _platformCallbacksHandler); _controller.complete(controller); if (widget.onWebViewCreated != null) { - widget.onWebViewCreated(controller); + widget.onWebViewCreated!(controller); } } void _assertJavascriptChannelNamesAreUnique() { if (widget.javascriptChannels == null || - widget.javascriptChannels.isEmpty) { + widget.javascriptChannels!.isEmpty) { return; } - assert(_extractChannelNames(widget.javascriptChannels).length == - widget.javascriptChannels.length); + assert(_extractChannelNames(widget.javascriptChannels)?.length == + widget.javascriptChannels!.length); } } @@ -473,7 +467,7 @@ WebSettings _webSettingsFromWidget(WebView widget) { darkMode: widget.darkMode, debuggingEnabled: widget.debuggingEnabled, gestureNavigationEnabled: widget.gestureNavigationEnabled, - userAgent: WebSetting.of(widget.userAgent), + userAgent: WebSetting.of(widget.userAgent ?? ''), ); } @@ -491,10 +485,10 @@ WebSettings _clearUnchangedWebSettings( assert(newValue.debuggingEnabled != null); assert(newValue.userAgent.isPresent); - JavascriptMode javascriptMode; - bool hasNavigationDelegate; - bool darkMode; - bool debuggingEnabled; + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? darkMode; + bool? debuggingEnabled; WebSetting userAgent = WebSetting.absent(); if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; @@ -521,7 +515,7 @@ WebSettings _clearUnchangedWebSettings( ); } -Set _extractChannelNames(Set channels) { +Set? _extractChannelNames(Set? channels) { final Set channelNames = channels == null ? {} : channels.map((JavascriptChannel channel) => channel.name).toSet(); @@ -541,15 +535,15 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { @override void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); + _javascriptChannels[channel]?.onMessageReceived(JavascriptMessage(message)); } @override - FutureOr onNavigationRequest({String url, bool isForMainFrame}) async { + FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) async { final NavigationRequest request = - NavigationRequest._(url: url, isForMainFrame: isForMainFrame); + NavigationRequest._(url: url!, isForMainFrame: isForMainFrame!); final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate(request) == + await _widget.navigationDelegate!(request) == NavigationDecision.navigate; return allowNavigation; } @@ -557,25 +551,25 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { @override void onPageStarted(String url) { if (_widget.onPageStarted != null) { - _widget.onPageStarted(url); + _widget.onPageStarted!(url); } } @override void onPageFinished(String url) { if (_widget.onPageFinished != null) { - _widget.onPageFinished(url); + _widget.onPageFinished!(url); } } @override void onWebResourceError(WebResourceError error) { if (_widget.onWebResourceError != null) { - _widget.onWebResourceError(error); + _widget.onWebResourceError!(error); } } - void _updateJavascriptChannelsFromSet(Set channels) { + void _updateJavascriptChannelsFromSet(Set? channels) { _javascriptChannels.clear(); if (channels == null) { return; @@ -595,7 +589,7 @@ class WebViewController { this._widget, this._webViewPlatformController, this._platformCallbacksHandler, - ) : assert(_webViewPlatformController != null) { + ) { _settings = _webSettingsFromWidget(_widget); } @@ -603,7 +597,7 @@ class WebViewController { final _PlatformCallbacksHandler _platformCallbacksHandler; - WebSettings _settings; + late WebSettings _settings; WebView _widget; @@ -617,15 +611,14 @@ class WebViewController { /// Throws an ArgumentError if `url` is not a valid URL string. Future loadUrl( String url, { - Map headers, + Map? headers, }) async { - assert(url != null); _validateUrlString(url); return _webViewPlatformController.loadUrl(url, headers); } Future loadData( - String baseUrl, + String? baseUrl, String data, String mimeType, String encoding) async { @@ -647,7 +640,7 @@ class WebViewController { /// current URL changes again by the time this function returns (in other /// words, by the time this future completes, the WebView may be displaying a /// different URL). - Future currentUrl() { + Future currentUrl() { return _webViewPlatformController.currentUrl(); } @@ -655,7 +648,7 @@ class WebViewController { /// /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has /// changed by the time the future completed. - Future canGoBack() { + Future canGoBack() { return _webViewPlatformController.canGoBack(); } @@ -663,7 +656,7 @@ class WebViewController { /// /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has /// changed by the time the future completed. - Future canGoForward() { + Future canGoForward() { return _webViewPlatformController.canGoForward(); } @@ -715,10 +708,10 @@ class WebViewController { } Future _updateJavascriptChannels( - Set newChannels) async { + Set? newChannels) async { final Set currentChannels = _platformCallbacksHandler._javascriptChannels.keys.toSet(); - final Set newChannelNames = _extractChannelNames(newChannels); + final Set newChannelNames = _extractChannelNames(newChannels)!; final Set channelsToAdd = newChannelNames.difference(currentChannels); final Set channelsToRemove = @@ -749,7 +742,7 @@ class WebViewController { /// When evaluating Javascript in a [WebView], it is best practice to wait for /// the [WebView.onPageFinished] callback. This guarantees all the Javascript /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { + Future evaluateJavascript(String? javascriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); @@ -765,7 +758,7 @@ class WebViewController { } /// Returns the title of the currently loaded page. - Future getTitle() { + Future getTitle() { return _webViewPlatformController.getTitle(); } @@ -786,14 +779,14 @@ class WebViewController { /// Return the horizontal scroll position, in WebView pixels, of this view. /// /// Scroll position is measured from left. - Future getScrollX() { + Future getScrollX() { return _webViewPlatformController.getScrollX(); } /// Return the vertical scroll position, in WebView pixels, of this view. /// /// Scroll position is measured from top. - Future getScrollY() { + Future getScrollY() { return _webViewPlatformController.getScrollY(); } } @@ -807,7 +800,7 @@ class CookieManager { CookieManager._(); - static CookieManager _instance; + static CookieManager? _instance; /// Clears all cookies for all [WebView] instances. /// diff --git a/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml b/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml index 1bd5564575..60fc18d241 100644 --- a/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml +++ b/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutte publish_to: none environment: - sdk: ">=2.8.0 <3.0.0" - flutter: "2.5.3" + sdk: ">=3.0.0 <3.10.6" + flutter: 3.13.2 dependencies: flutter: diff --git a/apps/flutter_parent/pubspec.lock b/apps/flutter_parent/pubspec.lock index c3268d5101..ee48ed2d57 100644 --- a/apps/flutter_parent/pubspec.lock +++ b/apps/flutter_parent/pubspec.lock @@ -5,308 +5,336 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" source: hosted - version: "22.0.0" + version: "61.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "5dce45a06d386358334eb1689108db6455d90ceb0d75848d5f4819283d4ee2b8" + url: "https://pub.dev" + source: hosted + version: "1.3.4" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "5.13.0" android_intent_plus: dependency: "direct main" description: name: android_intent_plus - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.2" - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" + sha256: f72ae20bb37108694f442e7ae6acbd28b453ca62ce86842f6787b784355abfe6 + url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.0.2" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" asn1lib: dependency: transitive description: name: asn1lib - url: "https://pub.dartlang.org" + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.11.0" barcode_scan2: dependency: "direct main" description: name: barcode_scan2 - url: "https://pub.dartlang.org" + sha256: "0b0625d27841a21e36e896195d86b2aada335e3c486f63647cce701495718e16" + url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.2.4" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.4.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" build_resolvers: dependency: "direct dev" description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.1" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.4.6" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.2.10" built_collection: dependency: "direct main" description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: "direct main" description: name: built_value - url: "https://pub.dartlang.org" + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.6.1" built_value_generator: dependency: "direct dev" description: name: built_value_generator - url: "https://pub.dartlang.org" + sha256: "14835d3ee2a0b19ffb263c57d82a3b2a64b0090d6b9d12e3b1646c1ff82a2476" + url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.6.1" cached_network_image: dependency: "direct main" description: name: cached_network_image - url: "https://pub.dartlang.org" + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.3" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" chewie: dependency: "direct main" description: name: chewie - url: "https://pub.dartlang.org" + sha256: "60701da1f22ed20cd2d40e856fd1f2249dacf5b629d9fa50676443a18a4857b8" + url: "https://pub.dev" source: hosted - version: "1.2.2" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "1.7.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.5.0" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.2" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" coverage: dependency: transitive description: name: coverage - url: "https://pub.dartlang.org" + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.6.3" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.3+4" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" source: hosted - version: "0.17.1" + version: "1.0.0" cupertino_icons: dependency: transitive description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.2" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" source: hosted - version: "0.5.6" - device_info: + version: "0.7.8" + device_info_plus: dependency: "direct main" description: - name: device_info - url: "https://pub.dartlang.org" + name: device_info_plus + sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" + url: "https://pub.dev" source: hosted - version: "2.0.3" - device_info_platform_interface: + version: "9.0.3" + device_info_plus_platform_interface: dependency: transitive description: - name: device_info_platform_interface - url: "https://pub.dartlang.org" + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "7.0.0" dio: dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" source: hosted - version: "4.0.1" - dio_http_cache: + version: "4.0.6" + dio_http_cache_lts: dependency: "direct main" description: - name: dio_http_cache - url: "https://pub.dartlang.org" + name: dio_http_cache_lts + sha256: "331da4c8444203bd3f20db1dc8a560969524a6b6e32e9ba16dad18f992a0b2b3" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.1" dio_smart_retry: dependency: "direct main" description: name: dio_smart_retry - url: "https://pub.dartlang.org" + sha256: "7c008542f7a5c5552a0757c7cd0e8c7b3131617120ee3e48456425f673f8ff09" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.4.0" email_validator: dependency: "direct main" description: name: email_validator - url: "https://pub.dartlang.org" + sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.17" encrypt: dependency: "direct main" description: name: encrypt - url: "https://pub.dartlang.org" + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" source: hosted version: "5.0.1" encrypted_shared_preferences: @@ -320,100 +348,154 @@ packages: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" faker: dependency: "direct main" description: name: faker - url: "https://pub.dartlang.org" + sha256: "746e59f91d8b06a389e74cf76e909a05ed69c12691768e2f93557fdf29200fd0" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.1.0" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" + url: "https://pub.dev" + source: hosted + version: "5.3.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "0.9.2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + url: "https://pub.dev" + source: hosted + version: "0.9.3" firebase_core: dependency: "direct main" description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: "2e9324f719e90200dc7d3c4f5d2abc26052f9f2b995d3b6626c47a0dfe1c8192" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "2.15.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: "0fd5c4b228de29b55fac38aed0d9e42514b3d3bd47675de52bf7f8fccaf922fa" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.6.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - url: "https://pub.dartlang.org" + sha256: "3607b46342537f98df18b130b6f5ab25cee6981a3a782e1a7b121d04dfea3caa" + url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "3.3.4" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" + sha256: c63abeb87b18f6e6d4bf6bb3977f15d2d9281a049d93fe098e83e56dcbf7da06 + url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.6.4" firebase_remote_config: dependency: "direct main" description: name: firebase_remote_config - url: "https://pub.dartlang.org" + sha256: "2883fad38d9536487f649c558c9e902a8ae36676990e9000bc8bd72f914ac9dd" + url: "https://pub.dev" source: hosted - version: "0.11.0+2" + version: "4.2.4" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface - url: "https://pub.dartlang.org" + sha256: "250384e15b4a732d9cca97b8b9ed5a29767f5c77d6301fc653d6bbe8b26b9487" + url: "https://pub.dev" + source: hosted + version: "1.4.4" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + sha256: c3e2be7d6e1416e4a1c7cc16d55a060a623e9e84c0ca0b07d9252584f39b7b08 + url: "https://pub.dev" source: hosted - version: "0.3.0+7" + version: "1.4.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" fluro: dependency: "direct main" description: name: fluro - url: "https://pub.dartlang.org" + sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" flutter: dependency: "direct main" description: flutter @@ -423,23 +505,26 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.3.1" flutter_downloader: dependency: "direct main" description: name: flutter_downloader - url: "https://pub.dartlang.org" + sha256: "2b126083d2e6b7c09755bca12012c4c734bcf7666cf07ba00c508fcb83e8d0d7" + url: "https://pub.dev" source: hosted - version: "1.7.4" + version: "1.11.1" flutter_driver: dependency: "direct dev" description: flutter @@ -449,30 +534,34 @@ packages: dependency: "direct main" description: name: flutter_linkify - url: "https://pub.dartlang.org" + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975" + url: "https://pub.dev" source: hosted - version: "9.0.2" + version: "15.1.0+1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "7.0.0+1" flutter_localizations: dependency: "direct main" description: flutter @@ -482,23 +571,26 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.15" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - url: "https://pub.dartlang.org" + sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be + url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "3.0.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" source: hosted - version: "0.23.0+1" + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter @@ -513,9 +605,10 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "3.2.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -525,436 +618,578 @@ packages: dependency: "direct main" description: name: get_it - url: "https://pub.dartlang.org" + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" + url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "7.6.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.1" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.4" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "1.1.0" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + sha256: "841837258e0b42c80946c43443054fc726f5e8aa84a97f363eb9ef0d45b33c14" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + url: "https://pub.dev" source: hosted - version: "0.8.4+4" + version: "0.8.7+4" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "8b6c160cdbe572199103a091c783685b236110e4a0fd7a4947f32ff5b7da8765" + url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "3.0.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + url: "https://pub.dev" + source: hosted + version: "0.8.8" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + url: "https://pub.dev" + source: hosted + version: "0.2.1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.9.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + url: "https://pub.dev" + source: hosted + version: "0.2.1" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" intl_generator: dependency: "direct dev" description: name: intl_generator - url: "https://pub.dartlang.org" + sha256: "120e03ccefe0c215801e44a8ccbaeabe6c895c5792f44298133745ab0a0e0bf5" + url: "https://pub.dev" source: hosted - version: "0.2.0+0" + version: "0.4.1" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.8.1" json_serializable: dependency: transitive description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "6.7.1" linkify: dependency: transitive description: name: linkify - url: "https://pub.dartlang.org" + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.0.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" source: hosted - version: "0.12.10" + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: "direct main" description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" source: hosted - version: "5.0.15" + version: "5.4.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" node_preamble: dependency: transitive description: name: node_preamble - url: "https://pub.dartlang.org" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted - version: "2.0.2" - package_info: + version: "2.1.0" + package_info_plus: dependency: "direct main" description: - name: package_info - url: "https://pub.dartlang.org" + name: package_info_plus + sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + url: "https://pub.dev" source: hosted - version: "2.0.2" - path: + version: "4.1.0" + package_info_plus_platform_interface: dependency: transitive description: - name: path - url: "https://pub.dartlang.org" + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" source: hosted - version: "1.8.0" - path_drawing: + version: "2.0.1" + path: dependency: transitive description: - name: path_drawing - url: "https://pub.dartlang.org" + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "0.5.1+1" + version: "1.8.3" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" + url: "https://pub.dev" source: hosted - version: "2.0.6" - path_provider_linux: + version: "2.1.0" + path_provider_android: dependency: transitive description: - name: path_provider_linux - url: "https://pub.dartlang.org" + name: path_provider_android + sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + url: "https://pub.dev" source: hosted version: "2.1.0" - path_provider_macos: + path_provider_foundation: dependency: transitive description: - name: path_provider_macos - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.3.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + url: "https://pub.dev" + source: hosted + version: "2.2.0" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.3" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" + sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "2.2.0" percent_indicator: dependency: "direct main" description: name: percent_indicator - url: "https://pub.dartlang.org" + sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "4.2.3" permission_handler: dependency: "direct main" description: name: permission_handler - url: "https://pub.dartlang.org" + sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" + url: "https://pub.dev" + source: hosted + version: "10.4.3" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" + url: "https://pub.dev" + source: hosted + version: "10.3.3" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" source: hosted - version: "8.2.5" + version: "9.1.4" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - url: "https://pub.dartlang.org" + sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" + url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "3.11.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "5.4.0" photo_view: dependency: "direct main" description: name: photo_view - url: "https://pub.dartlang.org" + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.14.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.5" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.4" protobuf: dependency: transitive description: name: protobuf - url: "https://pub.dartlang.org" + sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.3" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted - version: "0.27.2" + version: "0.27.7" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + url: "https://pub.dev" source: hosted - version: "2.0.8" - shared_preferences_linux: + version: "2.2.0" + shared_preferences_android: dependency: transitive description: - name: shared_preferences_linux - url: "https://pub.dartlang.org" + name: shared_preferences_android + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + url: "https://pub.dev" source: hosted - version: "2.0.2" - shared_preferences_macos: + version: "2.2.0" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" + name: shared_preferences_foundation + sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.3.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + url: "https://pub.dev" + source: hosted + version: "2.3.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.3.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.3.0" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - url: "https://pub.dartlang.org" + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.2" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -964,254 +1199,322 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.3.4" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - url: "https://pub.dartlang.org" + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" source_maps: dependency: transitive description: name: source_maps - url: "https://pub.dartlang.org" + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" source: hosted - version: "0.10.10" + version: "0.10.12" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.0" sqflite: dependency: "direct main" description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" source: hosted - version: "2.0.0+4" + version: "2.3.0" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + url: "https://pub.dev" source: hosted - version: "2.0.1+1" + version: "2.5.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" sync_http: dependency: transitive description: name: sync_http - url: "https://pub.dartlang.org" + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: "direct dev" description: name: test - url: "https://pub.dartlang.org" + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + url: "https://pub.dev" source: hosted - version: "1.17.10" + version: "1.24.3" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.6.0" test_core: dependency: transitive description: name: test_core - url: "https://pub.dartlang.org" + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.3" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.2" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" transparent_image: dependency: "direct main" description: name: transparent_image - url: "https://pub.dartlang.org" + sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.2" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" uuid: dependency: "direct main" description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "1.1.7" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + url: "https://pub.dev" + source: hosted + version: "1.1.7" vector_math: dependency: "direct main" description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" video_player: dependency: "direct main" description: name: video_player - url: "https://pub.dartlang.org" + sha256: "3fd106c74da32f336dc7feb65021da9b0207cb3124392935f1552834f7cce822" + url: "https://pub.dev" source: hosted - version: "2.2.6" - video_player_platform_interface: + version: "2.7.0" + video_player_android: dependency: transitive description: - name: video_player_platform_interface - url: "https://pub.dartlang.org" + name: video_player_android + sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + url: "https://pub.dev" source: hosted - version: "4.2.0" - video_player_web: + version: "2.4.9" + video_player_avfoundation: dependency: transitive description: - name: video_player_web - url: "https://pub.dartlang.org" + name: video_player_avfoundation + sha256: f5f5b7fe8c865be8a57fe80c2dca130772e1db775b7af4e5c5aa1905069cfc6c + url: "https://pub.dev" source: hosted - version: "2.0.4" - vm_service: + version: "2.4.9" + video_player_platform_interface: dependency: transitive description: - name: vm_service - url: "https://pub.dartlang.org" + name: video_player_platform_interface + sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + url: "https://pub.dev" source: hosted - version: "7.1.1" - wakelock: + version: "6.2.0" + video_player_web: dependency: transitive description: - name: wakelock - url: "https://pub.dartlang.org" + name: video_player_web + sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + url: "https://pub.dev" source: hosted - version: "0.5.6" - wakelock_macos: + version: "2.0.16" + vm_service: dependency: transitive description: - name: wakelock_macos - url: "https://pub.dartlang.org" + name: vm_service + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f + url: "https://pub.dev" source: hosted - version: "0.4.0" - wakelock_platform_interface: + version: "11.7.1" + wakelock_plus: dependency: transitive description: - name: wakelock_platform_interface - url: "https://pub.dartlang.org" + name: wakelock_plus + sha256: aac3f3258f01781ec9212df94eecef1eb9ba9350e106728def405baa096ba413 + url: "https://pub.dev" source: hosted - version: "0.3.0" - wakelock_web: + version: "1.1.1" + wakelock_plus_platform_interface: dependency: transitive description: - name: wakelock_web - url: "https://pub.dartlang.org" + name: wakelock_plus_platform_interface + sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + url: "https://pub.dev" source: hosted - version: "0.4.0" - wakelock_windows: + version: "1.1.0" + watcher: dependency: transitive description: - name: wakelock_windows - url: "https://pub.dartlang.org" + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" source: hosted - version: "0.2.0" - watcher: + version: "1.1.0" + web: dependency: transitive description: - name: watcher - url: "https://pub.dartlang.org" + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" webdriver: dependency: transitive description: name: webdriver - url: "https://pub.dartlang.org" + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.0" webview_flutter: dependency: "direct main" description: @@ -1223,30 +1526,42 @@ packages: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + url: "https://pub.dev" + source: hosted + version: "5.0.6" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "1.1.1" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "1.0.2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.5.3" + dart: ">=3.1.0-185.0.dev <3.10.6" + flutter: ">=3.13.2" diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index d43bf36c23..66d9770a5f 100644 --- a/apps/flutter_parent/pubspec.yaml +++ b/apps/flutter_parent/pubspec.yaml @@ -25,75 +25,76 @@ description: Canvas Parent # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.8.1+47 +version: 3.9.0+48 module: androidX: true environment: - sdk: ">=2.8.0 <3.0.0" - flutter: 2.5.3 + sdk: ">=3.0.0 <=3.13.2" + flutter: 3.13.2 dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - firebase_remote_config: ^0.11.0+2 - firebase_core: ^1.8.0 - firebase_crashlytics: ^2.2.4 - get_it: 6.1.1 - intl: ^0.17.0 - provider: ^5.0.0 - vector_math: ^2.1.0 - tuple: ^2.0.0 - flutter_slidable: ^0.6.0 - percent_indicator: ^3.4.0 - sqflite: ^2.0.0+4 - faker: ^2.0.0 - uuid: ^3.0.5 - collection: ^1.15.0 - flutter_linkify: ^5.0.2 - email_validator: ^2.0.1 + firebase_remote_config: ^4.2.4 + firebase_core: ^2.15.0 + firebase_crashlytics: ^3.3.4 + get_it: ^7.6.0 + intl: ^0.18.1 + provider: ^6.0.5 + vector_math: ^2.1.4 + tuple: ^2.0.2 + flutter_slidable: ^3.0.0 + percent_indicator: ^4.2.3 + sqflite: ^2.2.8+4 + faker: ^2.1.0 + uuid: ^3.0.7 + collection: ^1.17.2 + flutter_linkify: ^6.0.0 + email_validator: ^2.1.17 # File handling - path_provider: ^2.0.6 - flutter_downloader: 1.7.4 - mime: ^1.0.1 - file_picker: ^4.2.0 + path_provider: ^2.0.15 + flutter_downloader: ^1.11.1 + mime: ^1.0.4 + file_picker: ^5.3.2 # Media handling - flutter_svg: ^0.23.0+1 - image_picker: ^0.8.4+4 - transparent_image: ^2.0.0 - cached_network_image: ^3.1.0 - photo_view: ^0.13.0 - video_player: ^2.2.6 - chewie: ^1.2.2 - barcode_scan2: 4.1.4 + flutter_svg: ^2.0.7 + image_picker: ^1.0.1 + transparent_image: ^2.0.1 + cached_network_image: ^3.2.3 + photo_view: ^0.14.0 + video_player: ^2.7.0 + chewie: ^1.7.0 + barcode_scan2: ^4.2.4 # Networking / Serialization - dio: ^4.0.1 - dio_http_cache: ^0.3.0 - dio_smart_retry: ^1.0.3 - built_value: ^8.1.3 + dio: ^4.0.0 + dio_smart_retry: ^1.3.2 + built_value: ^8.6.1 built_collection: ^5.1.1 + dio_http_cache_lts: ^0.4.1 + # Platform interactions - android_intent_plus: ^3.0.2 - device_info: ^2.0.3 + android_intent_plus: ^4.0.1 + device_info_plus: ^9.0.2 encrypted_shared_preferences: # Used by ApiPrefs to securely store data path: ./plugins/encrypted_shared_preferences - flutter_local_notifications: ^9.0.2 - package_info: ^2.0.2 - permission_handler: ^8.2.5 - shared_preferences: ^2.0.8 # Used to cache remote config properties + flutter_local_notifications: ^15.1.0+1 + package_info_plus: ^4.0.2 + permission_handler: ^10.4.3 + shared_preferences: ^2.2.0 # Used to cache remote config properties #webview_flutter: 0.3.19+5 webview_flutter: # TODO: Remove once the flutter plugin supports baseUrl https://github.com/flutter/plugins/pull/2463 path: ./plugins/webview_flutter # Routing - fluro: ^2.0.3 + fluro: ^2.0.5 encrypt: ^5.0.1 @@ -102,13 +103,13 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - test: ^1.17.10 - intl_generator: ^0.2.0+0 - mockito: ^5.0.15 + test: ^1.24.3 + intl_generator: ^0.4.1 + mockito: ^5.4.2 - build_resolvers: ^2.0.4 - build_runner: ^2.1.4 - built_value_generator: ^8.1.1 + build_resolvers: ^2.2.1 + build_runner: ^2.4.6 + built_value_generator: ^8.6.1 # For information on the generic Dart part of this file, see the diff --git a/apps/flutter_parent/test/l10n/app_localizations_delegate_test.dart b/apps/flutter_parent/test/l10n/app_localizations_delegate_test.dart index 515d85913d..b25cd328cf 100644 --- a/apps/flutter_parent/test/l10n/app_localizations_delegate_test.dart +++ b/apps/flutter_parent/test/l10n/app_localizations_delegate_test.dart @@ -135,7 +135,7 @@ void main() { }); } -_testLocaleResolution(Locale fallback, Locale resolving, Locale expected, {bool matchCountry = true}) { +_testLocaleResolution(Locale? fallback, Locale resolving, Locale expected, {bool matchCountry = true}) { final callback = AppLocalizations.delegate.resolution(fallback: fallback, matchCountry: matchCountry); final actual = callback(resolving, AppLocalizations.delegate.supportedLocales); diff --git a/apps/flutter_parent/test/models/assignment_test.dart b/apps/flutter_parent/test/models/assignment_test.dart index 6379e111fd..3dead7d91c 100644 --- a/apps/flutter_parent/test/models/assignment_test.dart +++ b/apps/flutter_parent/test/models/assignment_test.dart @@ -78,12 +78,12 @@ void main() { group('getStatus', () { test('returns NONE for none submission type', () { final assignment = _mockAssignment(types: [SubmissionTypes.none]); - expect(assignment.getStatus(), SubmissionStatus.NONE); + expect(assignment.getStatus(studentId: studentId), SubmissionStatus.NONE); }); test('returns NONE for on paper submission type', () { final assignment = _mockAssignment(types: [SubmissionTypes.onPaper]); - expect(assignment.getStatus(), SubmissionStatus.NONE); + expect(assignment.getStatus(studentId: studentId), SubmissionStatus.NONE); }); test('returns LATE for a late submission', () { @@ -100,7 +100,7 @@ void main() { final past = DateTime.now().subtract(Duration(seconds: 1)); final assignment = _mockAssignment(dueAt: past, submission: null); - expect(assignment.getStatus(), SubmissionStatus.MISSING); + expect(assignment.getStatus(studentId: studentId), SubmissionStatus.MISSING); }); test('returns MISSING for a pass due assignment with an empty (server generated) submission', () { @@ -118,7 +118,7 @@ void main() { final assignment = _mockAssignment(dueAt: past, submission: null) .rebuild((b) => b..submissionTypes = BuiltList.of([SubmissionTypes.externalTool]).toBuilder()); - expect(assignment.getStatus(), SubmissionStatus.NOT_SUBMITTED); + expect(assignment.getStatus(studentId: studentId), SubmissionStatus.NOT_SUBMITTED); }); test('returns NOT_SUBMITTED for a submission with no submitted at time', () { @@ -214,8 +214,8 @@ void main() { } Assignment _mockAssignment({ - DateTime dueAt, - SubmissionBuilder submission, + DateTime? dueAt, + SubmissionBuilder? submission, List types = const [SubmissionTypes.onlineTextEntry], }) { List submissionList = submission != null ? [submission.build()] : []; diff --git a/apps/flutter_parent/test/models/attachment_test.dart b/apps/flutter_parent/test/models/attachment_test.dart index e10cd8abb2..b492ddb06a 100644 --- a/apps/flutter_parent/test/models/attachment_test.dart +++ b/apps/flutter_parent/test/models/attachment_test.dart @@ -19,7 +19,7 @@ import 'package:flutter_parent/utils/design/canvas_icons.dart'; import 'package:test/test.dart'; void main() { - IconData getIcon(String contentType) => Attachment((a) => a..contentType = contentType).getIcon(); + IconData getIcon(String? contentType) => Attachment((a) => a..contentType = contentType).getIcon(); test('returns video icon for video attachments', () { expect(getIcon('video/mp4'), CanvasIcons.video); diff --git a/apps/flutter_parent/test/models/course_grade_test.dart b/apps/flutter_parent/test/models/course_grade_test.dart index 6936db7fd6..ec233efb9c 100644 --- a/apps/flutter_parent/test/models/course_grade_test.dart +++ b/apps/flutter_parent/test/models/course_grade_test.dart @@ -41,7 +41,7 @@ void main() { test('returns true if hasGradingPeriods is true and there are no course enrollments', () { final course = Course((b) => b ..hasGradingPeriods = true - ..enrollments = BuiltList.of(List()).toBuilder()); + ..enrollments = BuiltList.of([]).toBuilder()); final grade = CourseGrade(course, null); expect(grade.isCourseGradeLocked(), true); diff --git a/apps/flutter_parent/test/models/course_test.dart b/apps/flutter_parent/test/models/course_test.dart index ef7b2cdee4..6e99de6055 100644 --- a/apps/flutter_parent/test/models/course_test.dart +++ b/apps/flutter_parent/test/models/course_test.dart @@ -39,7 +39,7 @@ void main() { final course = _course.rebuild((b) => b..enrollments = ListBuilder([_enrollment])); final grade = course.getCourseGrade(_studentId); - expect(grade, CourseGrade(course, course.enrollments.first, forceAllPeriods: false)); + expect(grade, CourseGrade(course, course.enrollments!.first, forceAllPeriods: false)); }); test('returns a course grade with the course and student gradinig period enrollment', () { diff --git a/apps/flutter_parent/test/models/schedule_item_test.dart b/apps/flutter_parent/test/models/schedule_item_test.dart index f35713b8c0..899ee112af 100644 --- a/apps/flutter_parent/test/models/schedule_item_test.dart +++ b/apps/flutter_parent/test/models/schedule_item_test.dart @@ -174,7 +174,7 @@ void main() { final result = item.getPlannerSubmission(); expect(result, isNotNull); - expect(result.submitted, isFalse); + expect(result!.submitted, isFalse); }); test('returns valid PlannerSubmission for valid submission', () { @@ -212,9 +212,9 @@ void main() { final plannable = Plannable((b) => b ..id = item.id ..title = item.title - ..pointsPossible = newAssignment?.pointsPossible - ..dueAt = newAssignment?.dueAt - ..assignmentId = newAssignment?.id); + ..pointsPossible = newAssignment.pointsPossible + ..dueAt = newAssignment.dueAt + ..assignmentId = newAssignment.id); final expectedResult = PlannerItem((b) => b ..courseId = contextId ..contextType = contextType diff --git a/apps/flutter_parent/test/models/submission_wrapper_test.dart b/apps/flutter_parent/test/models/submission_wrapper_test.dart index 3ef8b31611..38fcf6c074 100644 --- a/apps/flutter_parent/test/models/submission_wrapper_test.dart +++ b/apps/flutter_parent/test/models/submission_wrapper_test.dart @@ -57,10 +57,10 @@ void main() { test('Single submission', () { final encodedJson = jsonDecode(submissionString2); - SubmissionWrapper submissionWrapper = jsonSerializers.deserializeWith(SubmissionWrapper.serializer, encodedJson); + SubmissionWrapper? submissionWrapper = jsonSerializers.deserializeWith(SubmissionWrapper.serializer, encodedJson); - expect(submissionWrapper.submission, equals(submission2)); - expect(submissionWrapper.submissionList, isNull); + expect(submissionWrapper?.submission, equals(submission2)); + expect(submissionWrapper?.submissionList, isNull); }); test('List of submissions', () { @@ -68,10 +68,10 @@ void main() { final encodedJson = jsonDecode(jsonArray); print(encodedJson.toString()); - SubmissionWrapper submissionWrapper = jsonSerializers.deserializeWith(SubmissionWrapper.serializer, encodedJson); + SubmissionWrapper? submissionWrapper = jsonSerializers.deserializeWith(SubmissionWrapper.serializer, encodedJson); - expect(submissionWrapper.submission, isNull); - expect(submissionWrapper.submissionList, equals([submission1, submission2])); + expect(submissionWrapper?.submission, isNull); + expect(submissionWrapper?.submissionList, equals([submission1, submission2])); }); }); diff --git a/apps/flutter_parent/test/network/analytics_observer_test.dart b/apps/flutter_parent/test/network/analytics_observer_test.dart index bd5bf19ed1..56465151b0 100644 --- a/apps/flutter_parent/test/network/analytics_observer_test.dart +++ b/apps/flutter_parent/test/network/analytics_observer_test.dart @@ -20,6 +20,7 @@ import 'package:test/test.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final observer = AnalyticsObserver(); @@ -43,8 +44,8 @@ void main() { final settings = RouteSettings(name: PandaRouter.courseDetails(courseId)); observer.didPop( - MaterialPageRoute(builder: (_) => null), - MaterialPageRoute(builder: (_) => null, settings: settings), + MaterialPageRoute(builder: (_) => Container()), + MaterialPageRoute(builder: (_) => Container(), settings: settings), ); verify(analytics.setCurrentScreen(screenName)); @@ -68,7 +69,7 @@ void main() { final courseId = '1234'; final settings = RouteSettings(name: PandaRouter.courseDetails(courseId)); - observer.didReplace(newRoute: MaterialPageRoute(builder: (_) => null, settings: settings)); + observer.didReplace(newRoute: MaterialPageRoute(builder: (_) => Container(), settings: settings)); verify(analytics.setCurrentScreen(screenName)); verify(analytics.logMessage('Pushing widget: $screenName with params: {courseId: [$courseId]}')); @@ -77,7 +78,7 @@ void main() { test('does not log analytics with a non PageRoute', () { observer.didReplace( newRoute: _NonPageRoute(), - oldRoute: MaterialPageRoute(builder: (_) => null), + oldRoute: MaterialPageRoute(builder: (_) => Container()), ); verifyNever(analytics.setCurrentScreen(any)); @@ -92,8 +93,8 @@ void main() { final settings = RouteSettings(name: PandaRouter.courseDetails(courseId)); observer.didPush( - MaterialPageRoute(builder: (_) => null, settings: settings), - MaterialPageRoute(builder: (_) => null), + MaterialPageRoute(builder: (_) => Container(), settings: settings), + MaterialPageRoute(builder: (_) => Container()), ); verify(analytics.setCurrentScreen(screenName)); @@ -103,7 +104,7 @@ void main() { test('does not log analytics with a non PageRoute', () { observer.didPush( _NonPageRoute(), - MaterialPageRoute(builder: (_) => null), + MaterialPageRoute(builder: (_) => Container()), ); verifyNever(analytics.setCurrentScreen(any)); @@ -113,8 +114,8 @@ void main() { test('does not log analytics with a non null route name', () { observer.didPush( - MaterialPageRoute(builder: (_) => null, settings: RouteSettings()), - MaterialPageRoute(builder: (_) => null), + MaterialPageRoute(builder: (_) => Container(), settings: RouteSettings()), + MaterialPageRoute(builder: (_) => Container()), ); verifyNever(analytics.setCurrentScreen(any)); @@ -124,23 +125,23 @@ void main() { class _NonPageRoute extends ModalRoute { @override - bool get opaque => null; + bool get opaque => false; @override - Color get barrierColor => null; + Color? get barrierColor => null; @override - bool get maintainState => null; + bool get maintainState => false; @override - String get barrierLabel => null; + String? get barrierLabel => null; @override - bool get barrierDismissible => null; + bool get barrierDismissible => false; @override - Duration get transitionDuration => null; + Duration get transitionDuration => Duration.zero; @override - Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) => null; + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) => Container(); } diff --git a/apps/flutter_parent/test/network/api_prefs_test.dart b/apps/flutter_parent/test/network/api_prefs_test.dart index e7ec9e6397..bbae30e5a1 100644 --- a/apps/flutter_parent/test/network/api_prefs_test.dart +++ b/apps/flutter_parent/test/network/api_prefs_test.dart @@ -16,7 +16,6 @@ import 'dart:convert'; import 'dart:ui'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_parent/models/login.dart'; import 'package:flutter_parent/models/reminder.dart'; import 'package:flutter_parent/models/serializers.dart'; @@ -28,23 +27,20 @@ import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/canvas_model_utils.dart'; import '../utils/platform_config.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { tearDown(() { ApiPrefs.clean(); }); - test('is logged in throws error if not initiailzed', () { - expect(() => ApiPrefs.isLoggedIn(), throwsStateError); - }); - test('is logged in returns false', () async { await setupPlatformChannels(); expect(ApiPrefs.isLoggedIn(), false); @@ -192,7 +188,7 @@ void main() { await ApiPrefs.performLogout(switchingLogins: false); verify(reminderDb.getAllForUser(login.domain, login.user.id)); - verify(notificationUtil.deleteNotifications([reminder.id])); + verify(notificationUtil.deleteNotifications([reminder.id!])); verify(reminderDb.deleteAllForUser(login.domain, login.user.id)); verify(calendarFilterDb.deleteAllForUser(login.domain, login.user.id)); verify(authApi.deleteToken(login.domain, login.accessToken)); @@ -237,7 +233,7 @@ void main() { final user = CanvasModelTestUtils.mockUser(); await ApiPrefs.setUser(user); - expect(ApiPrefs.getCurrentLogin().masqueradeUser, user); + expect(ApiPrefs.getCurrentLogin()?.masqueradeUser, user); }); test('setting user updates with new locale rebuilds the app', () async { @@ -272,7 +268,7 @@ void main() { final user = CanvasModelTestUtils.mockUser(); await ApiPrefs.setUser(user); - expect(ApiPrefs.effectiveLocale(), Locale(user.effectiveLocale)); + expect(ApiPrefs.effectiveLocale(), Locale(user.effectiveLocale!)); }); test('effectiveLocale returns the users locale if effective locale is null', () async { @@ -286,7 +282,7 @@ void main() { await ApiPrefs.setUser(user); - expect(ApiPrefs.effectiveLocale(), Locale(user.locale)); + expect(ApiPrefs.effectiveLocale(), Locale(user.locale!)); }); test('effectiveLocale returns the users effective locale without inst if script is longer than 5', () async { @@ -315,10 +311,6 @@ void main() { ApiPrefs.effectiveLocale(), Locale.fromSubtags(languageCode: 'en', countryCode: 'GB', scriptCode: 'instukhe')); }); - test('getUser throws error if not initialized', () { - expect(() => ApiPrefs.getUser(), throwsStateError); - }); - test('getUser returns null', () async { await setupPlatformChannels(); expect(ApiPrefs.getUser(), null); @@ -346,10 +338,6 @@ void main() { expect(ApiPrefs.getDomain(), masqueradeDomain); }); - test('getHeaderMap throws state error', () { - expect(() => ApiPrefs.getHeaderMap(), throwsStateError); - }); - test('getHeaderMap returns a map with the accept-language from prefs', () async { final login = Login(); await setupPlatformChannels(config: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_LOGIN_UUID: login.uuid})); @@ -463,7 +451,7 @@ void main() { ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(currentStudent)), }); - SharedPreferences.setMockInitialValues(config.mockPrefs); + SharedPreferences.setMockInitialValues(config.mockPrefs!); EncryptedSharedPreferences.setMockInitialValues({}); final oldPrefs = await SharedPreferences.getInstance(); @@ -471,18 +459,17 @@ void main() { // Old prefs should not be null expect( - oldPrefs.getStringList(ApiPrefs.KEY_LOGINS).map((it) => deserialize(json.decode(it))).toList(), logins); + oldPrefs.getStringList(ApiPrefs.KEY_LOGINS)!.map((it) => deserialize(json.decode(it))).toList(), logins); expect(oldPrefs.getBool(ApiPrefs.KEY_HAS_MIGRATED), hasMigrated); expect(oldPrefs.getBool(ApiPrefs.KEY_HAS_CHECKED_OLD_REMINDERS), hasCheckedOldReminders); expect(oldPrefs.getString(ApiPrefs.KEY_CURRENT_LOGIN_UUID), currentLoginId); - expect(deserialize(json.decode(oldPrefs.get(ApiPrefs.KEY_CURRENT_STUDENT))), currentStudent); + expect(deserialize(json.decode(oldPrefs.get(ApiPrefs.KEY_CURRENT_STUDENT) as String)), currentStudent); // Actually do the test, init api prefs so we get the migration await ApiPrefs.init(); // encryptedPrefs should be not null - expect(encryptedPrefs.getStringList(ApiPrefs.KEY_LOGINS).map((it) => deserialize(json.decode(it))).toList(), - logins); + expect(encryptedPrefs.getStringList(ApiPrefs.KEY_LOGINS).map((it) => deserialize(json.decode(it))).toList(), logins); expect(encryptedPrefs.getBool(ApiPrefs.KEY_HAS_MIGRATED), hasMigrated); expect(encryptedPrefs.getBool(ApiPrefs.KEY_HAS_CHECKED_OLD_REMINDERS), hasCheckedOldReminders); expect(encryptedPrefs.getString(ApiPrefs.KEY_CURRENT_LOGIN_UUID), currentLoginId); @@ -498,7 +485,7 @@ void main() { } abstract class _Rebuildable { - void rebuild(Locale locale); + void rebuild(Locale? locale); } class _MockApp extends Mock implements _Rebuildable {} diff --git a/apps/flutter_parent/test/network/authentication_interceptor_test.dart b/apps/flutter_parent/test/network/authentication_interceptor_test.dart index 5e7306b9a9..918cc0a974 100644 --- a/apps/flutter_parent/test/network/authentication_interceptor_test.dart +++ b/apps/flutter_parent/test/network/authentication_interceptor_test.dart @@ -25,6 +25,7 @@ import 'package:test/test.dart'; import '../utils/platform_config.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final login = Login((b) => b @@ -36,7 +37,7 @@ void main() { final dio = MockDio(); final authApi = MockAuthApi(); final analytics = MockAnalytics(); - final errorHandler = _MockErrorHandler(); + final errorHandler = MockErrorInterceptorHandler(); final interceptor = AuthenticationInterceptor(dio); @@ -54,7 +55,8 @@ void main() { test('returns error if response code is not 401', () async { await setupPlatformChannels(); - final error = DioError(requestOptions: RequestOptions(path: 'accounts/self'), response: Response(statusCode: 403)); + var path = 'accounts/self'; + final error = DioError(requestOptions: RequestOptions(path: path), response: Response(statusCode: 403, requestOptions: RequestOptions(path: path))); // Test the error response await interceptor.onError(error, errorHandler); @@ -63,7 +65,8 @@ void main() { test('returns error if path is accounts/self', () async { await setupPlatformChannels(); - final error = DioError(requestOptions: RequestOptions(path: 'accounts/self'), response: Response(statusCode: 401)); + var path = 'accounts/self'; + final error = DioError(requestOptions: RequestOptions(path: path), response: Response(statusCode: 401, requestOptions: RequestOptions(path: path))); // Test the error response await interceptor.onError(error, errorHandler); @@ -73,8 +76,8 @@ void main() { test('returns error if headers have the retry header', () async { await setupPlatformChannels(config: PlatformConfig(initLoggedInUser: login)); final error = DioError( - requestOptions: RequestOptions(headers: {'mobile_refresh': 'mobile_refresh'}), - response: Response(statusCode: 401), + requestOptions: RequestOptions(path: '', headers: {'mobile_refresh': 'mobile_refresh'}), + response: Response(statusCode: 401, requestOptions: RequestOptions(path: '')), ); // Test the error response @@ -88,7 +91,7 @@ void main() { test('returns error if login is null', () async { await setupPlatformChannels(); - final error = DioError(requestOptions: RequestOptions(), response: Response(statusCode: 401)); + final error = DioError(requestOptions: RequestOptions(path: ''), response: Response(statusCode: 401, requestOptions: RequestOptions(path: ''))); // Test the error response await interceptor.onError(error, errorHandler); @@ -101,7 +104,7 @@ void main() { test('returns error if login client id is null', () async { await setupPlatformChannels(config: PlatformConfig(initLoggedInUser: login.rebuild((b) => b..clientId = null))); - final error = DioError(requestOptions: RequestOptions(), response: Response(statusCode: 401)); + final error = DioError(requestOptions: RequestOptions(path: ''), response: Response(statusCode: 401, requestOptions: RequestOptions(path: ''))); // Test the error response await interceptor.onError(error, errorHandler); @@ -114,7 +117,7 @@ void main() { test('returns error if login client secret is null', () async { await setupPlatformChannels(config: PlatformConfig(initLoggedInUser: login.rebuild((b) => b..clientSecret = null))); - final error = DioError(requestOptions: RequestOptions(), response: Response(statusCode: 401)); + final error = DioError(requestOptions: RequestOptions(path: ''), response: Response(statusCode: 401, requestOptions: RequestOptions(path: ''))); // Test the error response await interceptor.onError(error, errorHandler); @@ -127,10 +130,12 @@ void main() { test('returns error if the refresh api call failed', () async { await setupPlatformChannels(config: PlatformConfig(initLoggedInUser: login)); - final error = DioError(requestOptions: RequestOptions(), response: Response(statusCode: 401)); + final error = DioError(requestOptions: RequestOptions(path: ''), response: Response(statusCode: 401, requestOptions: RequestOptions(path: ''))); when(authApi.refreshToken()).thenAnswer((_) => Future.error('Failed to refresh')); + when(dio.interceptors).thenAnswer((realInvocation) => Interceptors()); + // Test the error response await interceptor.onError(error, errorHandler); verify(errorHandler.next(error)); @@ -147,7 +152,7 @@ void main() { final tokens = CanvasToken((b) => b..accessToken = 'token'); final path = 'test/path/stuff'; - final error = DioError(requestOptions: RequestOptions(path: path), response: Response(statusCode: 401)); + final error = DioError(requestOptions: RequestOptions(path: path), response: Response(statusCode: 401, requestOptions: RequestOptions(path: path))); final expectedOptions = RequestOptions(path: path, headers: { 'Authorization': 'Bearer ${tokens.accessToken}', 'mobile_refresh': 'mobile_refresh', @@ -156,6 +161,7 @@ void main() { when(authApi.refreshToken()).thenAnswer((_) async => tokens); when(dio.fetch(any)).thenAnswer((_) async => expectedAnswer); + when(dio.interceptors).thenAnswer((realInvocation) => Interceptors()); // Do the onError call await interceptor.onError(error, errorHandler); @@ -164,9 +170,7 @@ void main() { verify(authApi.refreshToken()).called(1); final actualOptions = verify(dio.fetch(captureAny)).captured[0] as RequestOptions; expect(actualOptions.headers, expectedOptions.headers); - expect(ApiPrefs.getCurrentLogin().accessToken, tokens.accessToken); + expect(ApiPrefs.getCurrentLogin()?.accessToken, tokens.accessToken); verifyNever(analytics.logEvent(any, extras: anyNamed('extras'))); }); } - -class _MockErrorHandler extends Mock implements ErrorInterceptorHandler {} diff --git a/apps/flutter_parent/test/network/dio_config_test.dart b/apps/flutter_parent/test/network/dio_config_test.dart index 6eaf03281a..c0a7a3591b 100644 --- a/apps/flutter_parent/test/network/dio_config_test.dart +++ b/apps/flutter_parent/test/network/dio_config_test.dart @@ -25,25 +25,10 @@ void main() { setUpAll(() async => await setupPlatformChannels()); test('returns a dio object', () async { - expect(canvasDio(), isA()); - }); - - group('base constructor asserts', () { - test('throws an error if baseUrl is null', () async { - expect(() => DioConfig(baseUrl: null), throwsAssertionError); - }); - - test('throws an error if cacheMaxAge is null', () async { - expect(() => DioConfig(cacheMaxAge: null), throwsAssertionError); - }); - - test('throws an error if forceRefresh is null', () async { - expect(() => DioConfig(forceRefresh: null), throwsAssertionError); - }); - - test('throws an error if pageSize is null', () async { - expect(() => DioConfig(pageSize: null), throwsAssertionError); - }); + final domain = 'https://test_domain.com'; + await ApiPrefs.switchLogins(Login((b) => b..domain = domain)); + var dio = await canvasDio(); + expect(dio, isA()); }); test('DioConfig.canvas returns a config object', () async { @@ -56,15 +41,18 @@ void main() { group('canvas options', () { test('initializes with a base url', () async { - final domain = 'test_domain'; + + final domain = 'https://test_domain.com'; await ApiPrefs.switchLogins(Login((b) => b..domain = domain)); - final options = canvasDio().options; + var dio = await canvasDio(); + final options = dio.options; expect(options.baseUrl, '$domain/api/v1/'); }); test('sets up headers', () async { - final options = canvasDio().options; + var dio = await canvasDio(); + final options = dio.options; final expectedHeaders = ApiPrefs.getHeaderMap() ..putIfAbsent('accept', () => 'application/json+canvas-string-ids') ..putIfAbsent('content-type', () => 'application/json; charset=utf-8'); @@ -75,11 +63,9 @@ void main() { final overrideToken = 'overrideToken'; final extras = {'other': 'value'}; - final options = canvasDio( - forceDeviceLanguage: true, - overrideToken: overrideToken, - extraHeaders: extras) - .options; + var dio = await canvasDio(forceDeviceLanguage: true, overrideToken: overrideToken, extraHeaders: extras); + + final options = dio.options; final expected = ApiPrefs.getHeaderMap( forceDeviceLanguage: true, token: overrideToken, extraHeaders: extras) ..putIfAbsent('accept', () => 'application/json+canvas-string-ids') @@ -90,7 +76,8 @@ void main() { test('sets per page param', () async { final perPageSize = 1; - final options = canvasDio(pageSize: PageSize(perPageSize)).options; + var dio = await canvasDio(pageSize: PageSize(perPageSize)); + final options = dio.options; expect(options.queryParameters, {'per_page': perPageSize}); }); @@ -98,44 +85,53 @@ void main() { test('sets as_user_id param when masquerading', () async { String userId = "masquerade_user_id"; final login = Login((b) => b - ..masqueradeDomain = 'masqueradeDomain' + ..masqueradeDomain = 'https://masqueradeDomain.com' ..masqueradeUser = CanvasModelTestUtils.mockUser(id: userId).toBuilder()); await ApiPrefs.switchLogins(login); - final options = canvasDio().options; + var dio = await canvasDio(); + + final options = dio.options; expect(options.queryParameters['as_user_id'], userId); }); test('Does not set as_user_id param when not masquerading', () async { - await ApiPrefs.switchLogins(Login()); - final options = canvasDio().options; + final domain = 'https://test_domain.com'; + await ApiPrefs.switchLogins(Login((b) => b..domain = domain)); + var dio = await canvasDio(); + final options = dio.options; expect(options.queryParameters.containsKey('as_user_id'), isFalse); }); test('sets cache extras', () async { - expect(canvasDio(forceRefresh: true).options.extra, isNotEmpty); + var dio = await canvasDio(forceRefresh: true); + expect(dio.options.extra, isNotEmpty); }); test('sets cache extras with force refrersh', () async { - expect(canvasDio(forceRefresh: true).options.extra['dio_cache_force_refresh'], isTrue); + var dio = await canvasDio(forceRefresh: true); + expect(dio.options.extra['dio_cache_force_refresh'], isTrue); }); }); group('core options', () { test('initializes with a base url', () async { - final options = DioConfig.core().dio.options; + var dio = await DioConfig.core().dio; + final options = dio.options; expect(options.baseUrl, 'https://canvas.instructure.com/api/v1/'); }); test('initializes with a base url without api path', () async { - final options = DioConfig.core(includeApiPath: false).dio.options; + var dio = await DioConfig.core(includeApiPath: false).dio; + final options = dio.options; expect(options.baseUrl, 'https://canvas.instructure.com/'); }); test('initializes with a beta base url', () async { - final options = DioConfig.core(useBetaDomain: true).dio.options; + var dio = await DioConfig.core(useBetaDomain: true).dio; + final options = dio.options; expect(options.baseUrl, 'https://canvas.beta.instructure.com/api/v1/'); }); @@ -144,42 +140,49 @@ void main() { '123': '123', 'content-type': 'application/json; charset=utf-8' }; - final options = DioConfig.core(headers: headers).dio.options; + var dio = await DioConfig.core(headers: headers).dio; + final options = dio.options; expect(options.headers, headers); }); test('sets per page param', () async { final perPageSize = 13; - final options = DioConfig.core(pageSize: PageSize(perPageSize)).dio.options; + var dio = await DioConfig.core(pageSize: PageSize(perPageSize)).dio; + final options = dio.options; expect(options.queryParameters, {'per_page': perPageSize}); }); test('sets up cache maxAge', () async { final age = Duration(minutes: 123); - final options = DioConfig.core(cacheMaxAge: age).dio.options; + var dio = await DioConfig.core(cacheMaxAge: age).dio; + final options = dio.options; expect(options.extra['dio_cache_max_age'], age); }); test('Does not set cache extras if max age is zero', () async { - final options = DioConfig.core(cacheMaxAge: Duration.zero).dio.options; + var dio = await DioConfig.core(cacheMaxAge: Duration.zero).dio; + final options = dio.options; expect(options.extra['dio_cache_max_age'], isNull); expect(options.extra['dio_cache_force_refresh'], isNull); }); test('sets cache extras with force refrersh', () async { - final options = DioConfig.core(cacheMaxAge: Duration(minutes: 1), forceRefresh: true).dio.options; + var dio = await DioConfig.core(cacheMaxAge: Duration(minutes: 1), forceRefresh: true).dio; + final options = dio.options; expect(options.extra['dio_cache_force_refresh'], isTrue); }); }); group('interceptors', () { test('adds cache manager', () async { + var dio = await canvasDio(); // The cache manager is an object that hooks in via an interceptor wrapper, so we can't check for the explicit type - expect(canvasDio().interceptors, contains(isA())); + expect(dio.interceptors, contains(isA())); }); test('adds log interceptor', () async { - expect(canvasDio().interceptors, contains(isA())); + var dio = await canvasDio(); + expect(dio.interceptors, contains(isA())); }); }); diff --git a/apps/flutter_parent/test/network/fetch_test.dart b/apps/flutter_parent/test/network/fetch_test.dart index 5f59718c72..74815027f7 100644 --- a/apps/flutter_parent/test/network/fetch_test.dart +++ b/apps/flutter_parent/test/network/fetch_test.dart @@ -21,11 +21,12 @@ import 'package:test/test.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { group('fetch', () { test('deserializes a response', () async { - final User response = await fetch(_request(_rawUser())); + final User? response = await fetch(_request(_rawUser())); expect(response, _getUser()); }); @@ -33,6 +34,7 @@ void main() { bool fail = false; await fetch(_requestFail()).catchError((_) { fail = true; // Don't return, just update the flag + return Future.value(null); }); expect(fail, isTrue); }); @@ -44,14 +46,15 @@ void main() { _getUser(id: '0'), _getUser(id: '1'), ]; - final PagedList response = await fetchFirstPage(_request(_rawUserList())); - expect(response.data, expected); + final PagedList? response = await fetchFirstPage(_request(_rawUserList())); + expect(response?.data, expected); }); test('catches errors and returns a Future.error', () async { bool fail = false; await fetchFirstPage(_requestFail()).catchError((_) { - fail = true; // Don't return, just update the flag + fail = true; + return Future.value(null); }); expect(fail, isTrue); }); @@ -64,7 +67,8 @@ void main() { await setupPlatformChannels(); bool fail = false; await fetchNextPage(null).catchError((_) { - fail = true; // Don't return, just update the flag + fail = true; + return Future.value(null); }); expect(fail, isTrue); }); @@ -76,7 +80,7 @@ void main() { _getUser(id: '0'), _getUser(id: '1'), ]; - final List response = await fetchList(_request(_rawUserList())); + final List? response = await fetchList(_request(_rawUserList())); expect(response, expected); }); @@ -100,7 +104,7 @@ void main() { when(dio.options).thenReturn(BaseOptions()); when(dio.get(pageUrl)).thenAnswer((_) => _request(_rawUserList(startIndex: 2))); - final List response = await fetchList(request, depaginateWith: dio); + final List? response = await fetchList(request, depaginateWith: dio); expect(response, expected); }); @@ -108,13 +112,14 @@ void main() { bool fail = false; await fetchList(_requestFail()).catchError((_) { fail = true; // Don't return, just update the flag + return Future.value(null); }); expect(fail, isTrue); }); }); } -Future> _request(data, {Headers headers}) async => Response(data: data, headers: headers); +Future> _request(data, {Headers? headers}) async => Response(data: data, headers: headers, requestOptions: RequestOptions(path: '')); Future> _requestFail() async => throw 'ErRoR'; diff --git a/apps/flutter_parent/test/network/paged_list_test.dart b/apps/flutter_parent/test/network/paged_list_test.dart index d9475c00ee..fe8f0e7c79 100644 --- a/apps/flutter_parent/test/network/paged_list_test.dart +++ b/apps/flutter_parent/test/network/paged_list_test.dart @@ -22,12 +22,12 @@ import 'package:test/test.dart'; void main() { test('has no data', () { - PagedList list = PagedList(Response()); + PagedList list = PagedList(Response(requestOptions: RequestOptions(path: ''))); expect(list.data, []); }); test('has no headers', () { - PagedList list = PagedList(Response()); + PagedList list = PagedList(Response(requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, null); }); @@ -35,7 +35,7 @@ void main() { final map = { 'key': ['value'] }; - PagedList list = PagedList(Response(headers: Headers.fromMap(map))); + PagedList list = PagedList(Response(headers: Headers.fromMap(map), requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, null); }); @@ -43,7 +43,7 @@ void main() { final map = { 'link': ['; rel="last"'] }; - PagedList list = PagedList(Response(headers: Headers.fromMap(map))); + PagedList list = PagedList(Response(headers: Headers.fromMap(map), requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, null); }); @@ -56,7 +56,7 @@ void main() { .trim() ] }); - PagedList list = PagedList(Response(headers: testHeaders)); + PagedList list = PagedList(Response(headers: testHeaders, requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, 'https://mobiledev.instructure.com/api/v1/courses/549835/assignments?include%5B%5D=rubric_assessment&needs_grading_count_by_section=true&order_by=position&override_assignment_dates=true&page=2&per_page=10'); }); @@ -70,10 +70,10 @@ void main() { .trim() ] }); - PagedList list = PagedList(Response()); + PagedList list = PagedList(Response(requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, null); - list.updateWithResponse(Response(headers: testHeaders)); + list.updateWithResponse(Response(headers: testHeaders, requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, 'https://mobiledev.instructure.com/api/v1/courses/549835/assignments?include%5B%5D=rubric_assessment&needs_grading_count_by_section=true&order_by=position&override_assignment_dates=true&page=2&per_page=10'); }); @@ -87,10 +87,10 @@ void main() { .trim() ] }); - PagedList list = PagedList(Response()); + PagedList list = PagedList(Response(requestOptions: RequestOptions(path: ''))); expect(list.nextUrl, null); - list.updateWithPagedList(PagedList(Response(headers: testHeaders))); + list.updateWithPagedList(PagedList(Response(headers: testHeaders, requestOptions: RequestOptions(path: '')))); expect(list.nextUrl, 'https://mobiledev.instructure.com/api/v1/courses/549835/assignments?include%5B%5D=rubric_assessment&needs_grading_count_by_section=true&order_by=position&override_assignment_dates=true&page=2&per_page=10'); }); @@ -108,7 +108,7 @@ void main() { ..domain = 'Domain $index'); }); final serializedData = serializer.serialize(BuiltList(data), specifiedType: type); - PagedList list = PagedList(Response(data: serializedData)); + PagedList list = PagedList(Response(data: serializedData, requestOptions: RequestOptions(path: ''))); expect(list.data, data); }); @@ -126,7 +126,7 @@ void main() { ..domain = 'Domain $index'); }); final serializedData = serializer.serialize(BuiltList(data), specifiedType: type); - PagedList list = PagedList(Response(data: serializedData)); + PagedList list = PagedList(Response(data: serializedData, requestOptions: RequestOptions(path: ''))); final dataAlt = List.generate(4, (index) { return SchoolDomain((builder) => builder @@ -135,7 +135,7 @@ void main() { }); final serializedDataAlt = serializer.serialize(BuiltList(dataAlt), specifiedType: type); - list.updateWithResponse(Response(data: serializedDataAlt)); + list.updateWithResponse(Response(data: serializedDataAlt, requestOptions: RequestOptions(path: ''))); expect(list.data, data + dataAlt); }); @@ -153,7 +153,7 @@ void main() { ..domain = 'Domain $index'); }); final serializedData = serializer.serialize(BuiltList(data), specifiedType: type); - PagedList list = PagedList(Response(data: serializedData)); + PagedList list = PagedList(Response(data: serializedData, requestOptions: RequestOptions(path: ''))); final dataAlt = List.generate(4, (index) { return SchoolDomain((builder) => builder @@ -161,7 +161,7 @@ void main() { ..domain = 'Alt Domain $index'); }); final serializedDataAlt = serializer.serialize(BuiltList(dataAlt), specifiedType: type); - PagedList listAlt = PagedList(Response(data: serializedDataAlt)); + PagedList listAlt = PagedList(Response(data: serializedDataAlt, requestOptions: RequestOptions(path: ''))); list.updateWithPagedList(listAlt); diff --git a/apps/flutter_parent/test/router/panda_router_test.dart b/apps/flutter_parent/test/router/panda_router_test.dart index e149dd5412..56b87aa149 100644 --- a/apps/flutter_parent/test/router/panda_router_test.dart +++ b/apps/flutter_parent/test/router/panda_router_test.dart @@ -32,7 +32,7 @@ import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart'; import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart'; import 'package:flutter_parent/screens/events/event_details_screen.dart'; import 'package:flutter_parent/screens/help/help_screen.dart'; -import 'package:flutter_parent/screens/help/legal_screen.dart'; +import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/screens/help/terms_of_use_screen.dart'; import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart'; import 'package:flutter_parent/screens/login_landing_screen.dart'; @@ -56,6 +56,7 @@ import '../utils/canvas_model_utils.dart'; import '../utils/platform_config.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; final _analytics = MockAnalytics(); @@ -67,9 +68,9 @@ void main() { ..accessToken = 'token' ..user = user.toBuilder()); - final _mockNav = MockNav(); + final _mockNav = MockQuickNav(); final _mockWebContentInteractor = MockWebContentInteractor(); - final _mockSnackbar = MockSnackbar(); + final _mockSnackbar = MockFlutterSnackbarVeneer(); final _mockLauncher = MockUrlLauncher(); setUpAll(() async { @@ -294,10 +295,10 @@ void main() { expect(widget, isA()); - DashboardScreen dashboard = widget; + DashboardScreen dashboard = widget as DashboardScreen; expect(dashboard.startingPage, DashboardContentScreens.Calendar); - expect(dashboard.deepLinkParams[CalendarScreen.startDateKey], DateTime(2018, 12, 15)); - expect(dashboard.deepLinkParams[CalendarScreen.startViewKey], CalendarView.Month); + expect(dashboard.deepLinkParams?[CalendarScreen.startDateKey], DateTime(2018, 12, 15)); + expect(dashboard.deepLinkParams?[CalendarScreen.startViewKey], CalendarView.Month); }); test('courses returns Dashboard screen', () { @@ -340,9 +341,9 @@ void main() { final pairingInfo = QRUtils.parsePairingInfo(uri) as QRPairingInfo; final widget = _getWidgetFromRoute(PandaRouter.qrPairing(pairingUri: uri)); expect(widget, isA()); - expect((widget as QRPairingScreen).pairingInfo.code, pairingInfo.code); - expect((widget as QRPairingScreen).pairingInfo.domain, pairingInfo.domain); - expect((widget as QRPairingScreen).pairingInfo.accountId, pairingInfo.accountId); + expect((widget as QRPairingScreen).pairingInfo?.code, pairingInfo.code); + expect((widget).pairingInfo?.domain, pairingInfo.domain); + expect((widget).pairingInfo?.accountId, pairingInfo.accountId); }); test('syllabus returns CourseRoutingShellScreen', () { @@ -358,7 +359,7 @@ void main() { final widget = _getWidgetFromRoute(PandaRouter.frontPage(courseId)); expect(widget, isA()); expect((widget as CourseRoutingShellScreen).courseId, courseId); - expect((widget as CourseRoutingShellScreen).type, CourseShellType.frontPage); + expect((widget).type, CourseShellType.frontPage); }); test('frontPageWiki returns CourseRoutingShellScreen', () { @@ -366,7 +367,7 @@ void main() { final widget = _getWidgetFromRoute(PandaRouter.frontPageWiki(courseId)); expect(widget, isA()); expect((widget as CourseRoutingShellScreen).courseId, courseId); - expect((widget as CourseRoutingShellScreen).type, CourseShellType.frontPage); + expect((widget).type, CourseShellType.frontPage); }); }); @@ -626,11 +627,11 @@ String _routerErrorRoute(String url) => '/error?url=${Uri.encodeQueryComponent(u String _simpleWebViewRoute(String url) => '/internal?url=${Uri.encodeQueryComponent(url)}'; -Widget _getWidgetFromRoute(String route, {Map> extraParams}) { +Widget _getWidgetFromRoute(String route, {Map>? extraParams}) { final match = PandaRouter.router.match(route); - if (extraParams != null) match.parameters.addAll(extraParams); - final widget = (match.route.handler as Handler).handlerFunc(null, match.parameters); + if (extraParams != null) match?.parameters.addAll(extraParams); + final widget = (match!.route.handler as Handler).handlerFunc(null, match.parameters); - return widget; + return widget!; } diff --git a/apps/flutter_parent/test/router/router_error_screen_test.dart b/apps/flutter_parent/test/router/router_error_screen_test.dart index a21f512a75..ec04b0bf93 100644 --- a/apps/flutter_parent/test/router/router_error_screen_test.dart +++ b/apps/flutter_parent/test/router/router_error_screen_test.dart @@ -14,20 +14,26 @@ * along with this program. If not, see . */ +import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; +import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/router/router_error_screen.dart'; import 'package:flutter_parent/screens/login_landing_screen.dart'; +import 'package:flutter_parent/utils/db/calendar_filter_db.dart'; +import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import '../screens/courses/course_summary_screen_test.dart'; import '../utils/accessibility_utils.dart'; import '../utils/platform_config.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final String _domain = 'https://test.instructure.com'; @@ -35,6 +41,7 @@ void main() { setUp(() async { final mockRemoteConfig = setupMockRemoteConfig(valueSettings: {'qr_login_enabled_parent': 'true'}); await setupPlatformChannels(config: PlatformConfig(initRemoteConfig: mockRemoteConfig)); + ApiPrefs.init(); }); tearDown(() { @@ -72,18 +79,20 @@ void main() { }); testWidgetsWithAccessibilityChecks('router error screen switch users', (tester) async { - setupTestLocator((locator) { - locator.registerLazySingleton(() => QuickNav()); - }); + setupTestLocator((locator) { + locator.registerLazySingleton(() => QuickNav()); + locator.registerLazySingleton(() => MockCalendarFilterDb()); + }); - await tester.pumpWidget(TestApp( - RouterErrorScreen(_domain), - )); - await tester.pumpAndSettle(); - await tester.tap(find.text(AppLocalizations().switchUsers)); - await tester.pumpAndSettle(); + await tester.pumpWidget(TestApp( + RouterErrorScreen(_domain), + )); + await tester.pumpAndSettle(); + await tester.tap(find.text(l10n.switchUsers)); + await tester.pumpAndSettle(); + + expect(find.byType(LoginLandingScreen), findsOneWidget); + expect(ApiPrefs.isLoggedIn(), false); - expect(find.byType(LoginLandingScreen), findsOneWidget); - expect(ApiPrefs.isLoggedIn(), false); }); } diff --git a/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart b/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart index d3d3779be5..600529acca 100644 --- a/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart +++ b/apps/flutter_parent/test/screens/account_creation/account_creation_interactor_test.dart @@ -15,11 +15,13 @@ import 'package:flutter_parent/network/api/accounts_api.dart'; import 'package:flutter_parent/screens/account_creation/account_creation_interactor.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final launcher = MockUrlLauncher(); diff --git a/apps/flutter_parent/test/screens/account_creation/account_creation_screen_test.dart b/apps/flutter_parent/test/screens/account_creation/account_creation_screen_test.dart index 375d9a86ad..1dab9d3388 100644 --- a/apps/flutter_parent/test/screens/account_creation/account_creation_screen_test.dart +++ b/apps/flutter_parent/test/screens/account_creation/account_creation_screen_test.dart @@ -34,6 +34,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/finders.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import '../courses/course_summary_screen_test.dart'; /** @@ -55,7 +56,7 @@ void main() { ..id = '123' ..passive = true); - final pairingInfo = QRPairingScanResult.success('123', 'hodor.com', '123'); + final pairingInfo = QRPairingScanResult.success('123', 'hodor.com', '123') as QRPairingInfo; final tosString = 'By tapping \'Create Account\', you agree to the Terms of Service and acknowledge the Privacy Policy'; @@ -87,7 +88,7 @@ void main() { expect(find.byIcon(CanvasIcons.eye), findsOneWidget); expect(find.text('Create Account', skipOffstage: false), findsOneWidget); - expect(find.byType(RaisedButton, skipOffstage: false), findsOneWidget); + expect(find.byType(ElevatedButton, skipOffstage: false), findsOneWidget); }, a11yExclusions: {A11yExclusion.minTapSize}); testWidgetsWithAccessibilityChecks('tos and privacy text visible when passive false', (tester) async { @@ -148,7 +149,7 @@ void main() { await tester.drag(find.byType(Scaffold), Offset(0, -500)); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text('Please enter full name'), findsNothing); @@ -166,7 +167,7 @@ void main() { await tester.drag(find.byType(Scaffold), Offset(0, -500)); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text('Please enter full name'), findsOneWidget); @@ -185,7 +186,7 @@ void main() { await tester.drag(find.byType(Scaffold), Offset(0, -500)); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text('Password must contain at least 8 characters'), findsOneWidget); @@ -202,7 +203,7 @@ void main() { await tester.drag(find.byType(Scaffold), Offset(0, -500)); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text('Please enter a valid email address'), findsOneWidget); @@ -235,12 +236,12 @@ void main() { // Get text selection for 'Canvas Support' span var targetText = l10n.qrCreateAccountTermsOfService; var bodyWidget = tester.widget(find.byKey(AccountCreationScreen.accountCreationTextSpanKey)); - var bodyText = bodyWidget.textSpan.toPlainText(); + var bodyText = bodyWidget.textSpan?.toPlainText() ?? ''; var index = bodyText.indexOf(targetText); var selection = TextSelection(baseOffset: index, extentOffset: index + targetText.length); // Get clickable area - RenderParagraph box = AccountCreationScreen.accountCreationTextSpanKey.currentContext.findRenderObject(); + RenderParagraph box = AccountCreationScreen.accountCreationTextSpanKey.currentContext?.findRenderObject() as RenderParagraph; var bodyOffset = box.localToGlobal(Offset.zero); var textOffset = box.getBoxesForSelection(selection)[0].toRect().center; @@ -261,12 +262,12 @@ void main() { // Get text selection for 'Canvas Support' span var targetText = l10n.qrCreateAccountTermsOfService; var bodyWidget = tester.widget(find.byKey(AccountCreationScreen.accountCreationTextSpanKey)); - var bodyText = bodyWidget.textSpan.toPlainText(); + var bodyText = bodyWidget.textSpan?.toPlainText() ?? ''; var index = bodyText.indexOf(targetText); var selection = TextSelection(baseOffset: index, extentOffset: index + targetText.length); // Get clickable area - RenderParagraph box = AccountCreationScreen.accountCreationTextSpanKey.currentContext.findRenderObject(); + RenderParagraph box = AccountCreationScreen.accountCreationTextSpanKey.currentContext?.findRenderObject() as RenderParagraph; var bodyOffset = box.localToGlobal(Offset.zero); var textOffset = box.getBoxesForSelection(selection)[0].toRect().center; @@ -287,12 +288,12 @@ void main() { // Get text selection for 'Canvas Support' span var targetText = l10n.qrCreateAccountPrivacyPolicy; var bodyWidget = tester.widget(find.byKey(AccountCreationScreen.accountCreationTextSpanKey)); - var bodyText = bodyWidget.textSpan.toPlainText(); + var bodyText = bodyWidget.textSpan?.toPlainText() ?? ''; var index = bodyText.indexOf(targetText); var selection = TextSelection(baseOffset: index, extentOffset: index + targetText.length); // Get clickable area - RenderParagraph box = AccountCreationScreen.accountCreationTextSpanKey.currentContext.findRenderObject(); + RenderParagraph box = AccountCreationScreen.accountCreationTextSpanKey.currentContext?.findRenderObject() as RenderParagraph; var bodyOffset = box.localToGlobal(Offset.zero); var textOffset = box.getBoxesForSelection(selection)[0].toRect().center; @@ -320,7 +321,7 @@ void main() { testWidgetsWithAccessibilityChecks('valid form account creation pushes login route', (tester) async { when(interactor.getToSForAccount('123', 'hodor.com')).thenAnswer((_) async => tos); when(interactor.createNewAccount('123', '123', 'hodor', 'hodor@hodor.com', '12345678', 'hodor.com')) - .thenAnswer((_) async => Response(statusCode: 200)); + .thenAnswer((_) async => Response(statusCode: 200, requestOptions: RequestOptions(path: ''))); await tester.pumpWidget(TestApp(AccountCreationScreen(pairingInfo))); await tester.pumpAndSettle(); @@ -333,7 +334,7 @@ void main() { await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); verify(mockNav.pushRoute(any, PandaRouter.loginWeb('hodor.com', loginFlow: LoginFlow.normal))); verify(analytics.logEvent( @@ -347,7 +348,7 @@ void main() { '{\"errors\":{\"user\":{},\"pseudonym\":{},\"observee\":{},\"pairing_code\":{\"code\":[{\"attribute\":\"code\",\"type\":\"invalid\",\"message\":\"invalid\"}]},\"recaptcha\":null}}'); when(interactor.getToSForAccount('123', 'hodor.com')).thenAnswer((_) async => tos); when(interactor.createNewAccount('123', '123', 'hodor', 'hodor@hodor.com', '12345678', 'hodor.com')) - .thenThrow(DioError(response: Response(data: jsonData))); + .thenThrow(DioError(response: Response(data: jsonData, requestOptions: RequestOptions(path: '')), requestOptions: RequestOptions(path: ''))); await tester.pumpWidget(TestApp(AccountCreationScreen(pairingInfo))); await tester.pumpAndSettle(); @@ -360,7 +361,7 @@ void main() { await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); await tester.pump(); @@ -378,7 +379,7 @@ void main() { '{\"errors\":{\"user\":{\"pseudonyms\":[{\"message\":\"invalid\"}]},\"pseudonym\":{},\"observee\":{},\"pairing_code\":{},\"recaptcha\":null}}'); when(interactor.getToSForAccount('123', 'hodor.com')).thenAnswer((_) async => tos); when(interactor.createNewAccount('123', '123', 'hodor', 'hodor@hodor.com', '12345678', 'hodor.com')) - .thenThrow(DioError(response: Response(data: jsonData))); + .thenThrow(DioError(response: Response(data: jsonData, requestOptions: RequestOptions(path: '')), requestOptions: RequestOptions(path: ''))); await tester.pumpWidget(TestApp(AccountCreationScreen(pairingInfo))); await tester.pumpAndSettle(); @@ -391,7 +392,7 @@ void main() { await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); await tester.pump(); @@ -408,7 +409,7 @@ void main() { testWidgetsWithAccessibilityChecks('account creation with error shows generic error message', (tester) async { when(interactor.getToSForAccount('123', 'hodor.com')).thenAnswer((_) async => tos); when(interactor.createNewAccount('123', '123', 'hodor', 'hodor@hodor.com', '12345678', 'hodor.com')) - .thenThrow(DioError(response: Response())); + .thenThrow(DioError(response: Response(requestOptions: RequestOptions(path: '')), requestOptions: RequestOptions(path: ''))); await tester.pumpWidget(TestApp(AccountCreationScreen(pairingInfo))); await tester.pumpAndSettle(); @@ -421,7 +422,7 @@ void main() { await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); await tester.pump(); diff --git a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_extensions_test.dart b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_extensions_test.dart index 560d6aa4da..1eab98dc90 100644 --- a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_extensions_test.dart +++ b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_extensions_test.dart @@ -43,7 +43,7 @@ void main() { test('Course grade low: null/course grade high', () { AlertType testingType = AlertType.courseGradeLow; String highValue = '42'; - List expectedResult = [null, highValue]; + List expectedResult = [null, highValue]; AlertThreshold courseGradeLow = _mockThreshold(testingType, value: '24'); AlertThreshold courseGradeHigh = _mockThreshold(AlertType.courseGradeHigh, value: highValue); List thresholds = [courseGradeLow, courseGradeHigh]; @@ -54,7 +54,7 @@ void main() { test('Course grade high: course grade low/null', () { AlertType testingType = AlertType.courseGradeHigh; String lowValue = '24'; - List expectedResult = [lowValue, null]; + List expectedResult = [lowValue, null]; AlertThreshold courseGradeLow = _mockThreshold(AlertType.courseGradeLow, value: lowValue); AlertThreshold courseGradeHigh = _mockThreshold(testingType, value: '42'); List thresholds = [courseGradeLow, courseGradeHigh]; @@ -65,7 +65,7 @@ void main() { test('Assignment grade low: null/assignment grade high', () { AlertType testingType = AlertType.assignmentGradeLow; String highValue = '42'; - List expectedResult = [null, highValue]; + List expectedResult = [null, highValue]; AlertThreshold assignmentGradeLow = _mockThreshold(testingType, value: '24'); AlertThreshold assignmentGradeHigh = _mockThreshold(AlertType.assignmentGradeHigh, value: highValue); List thresholds = [assignmentGradeLow, assignmentGradeHigh]; @@ -76,7 +76,7 @@ void main() { test('Assignment grade high: assignment grade low/null', () { AlertType testingType = AlertType.assignmentGradeHigh; String lowValue = '24'; - List expectedResult = [lowValue, null]; + List expectedResult = [lowValue, null]; AlertThreshold assignmentGradeLow = _mockThreshold(AlertType.assignmentGradeLow, value: lowValue); AlertThreshold assignmentGradeHigh = _mockThreshold(testingType, value: '42'); List thresholds = [assignmentGradeLow, assignmentGradeHigh]; @@ -86,7 +86,7 @@ void main() { }); } -AlertThreshold _mockThreshold(AlertType type, {String value}) => AlertThreshold((b) => b +AlertThreshold _mockThreshold(AlertType? type, {String? value}) => AlertThreshold((b) => b ..alertType = type ?? AlertType.courseGradeLow ..threshold = value ?? null ..build()); diff --git a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_interactor_test.dart b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_interactor_test.dart index 5ab7c8d2eb..d47139ca26 100644 --- a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_interactor_test.dart +++ b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_interactor_test.dart @@ -22,9 +22,10 @@ import 'package:mockito/mockito.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - AlertThreshold _mockThreshold(AlertType type, {String value}) => AlertThreshold((b) => b + AlertThreshold _mockThreshold(AlertType type, {String? value}) => AlertThreshold((b) => b ..alertType = type ..threshold = value ..build()); @@ -43,7 +44,7 @@ void main() { }); test('Switch created api call', () async { - when(api.createThreshold(any, any)).thenAnswer((_) => null); + when(api.createThreshold(any, any)).thenAnswer((_) => Future.value(null)); var type = AlertType.assignmentMissing; var alertThreshold = null; @@ -55,7 +56,7 @@ void main() { }); test('Switch deleted api call', () async { - when(api.deleteAlert(any)).thenAnswer((_) => null); + when(api.deleteAlert(any)).thenAnswer((_) => Future.value(null)); var type = AlertType.assignmentMissing; var alertThreshold = _mockThreshold(type); @@ -67,7 +68,7 @@ void main() { }); test('Percentage updated api call', () async { - when(api.createThreshold(any, any)).thenAnswer((_) => null); + when(api.createThreshold(any, any)).thenAnswer((_) => Future.value(null)); var type = AlertType.courseGradeLow; var value = '42'; @@ -80,7 +81,7 @@ void main() { }); test('Percentage deleted api call', () async { - when(api.deleteAlert(any)).thenAnswer((_) => null); + when(api.deleteAlert(any)).thenAnswer((_) => Future.value(null)); var type = AlertType.courseGradeLow; var value = '-1'; diff --git a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_percent_dialog_test.dart b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_percent_dialog_test.dart index 51eda816ab..0867bb1a8f 100644 --- a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_percent_dialog_test.dart +++ b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_percent_dialog_test.dart @@ -24,6 +24,7 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { group('Render', () { @@ -57,7 +58,7 @@ void main() { await tester.pumpWidget(widget); await tester.pumpAndSettle(); - expect(find.byType(FlatButton), findsNWidgets(3)); + expect(find.byType(TextButton), findsNWidgets(3)); expect(find.text(AppLocalizations().cancel.toUpperCase()), findsOneWidget); expect(find.text(AppLocalizations().never.toUpperCase()), findsOneWidget); expect(find.text(AppLocalizations().ok), findsOneWidget); @@ -179,7 +180,7 @@ void main() { await tester.pumpAndSettle(); // Check for error message - expect(tester.widget(find.byKey(AlertThresholdsPercentageDialogState.okButtonKey)).enabled, isFalse); + expect(tester.widget(find.byKey(AlertThresholdsPercentageDialogState.okButtonKey)).enabled, isFalse); }); testWidgets('disable ok button when setting low >= high', (tester) async { @@ -200,7 +201,7 @@ void main() { await tester.pumpAndSettle(); // Check for error message - expect(tester.widget(find.byKey(AlertThresholdsPercentageDialogState.okButtonKey)).enabled, isFalse); + expect(tester.widget(find.byKey(AlertThresholdsPercentageDialogState.okButtonKey)).enabled, isFalse); }); testWidgets('disable ok button when setting high <= low', (tester) async { @@ -221,7 +222,7 @@ void main() { await tester.pumpAndSettle(); // Check for error message - expect(tester.widget(find.byKey(AlertThresholdsPercentageDialogState.okButtonKey)).enabled, isFalse); + expect(tester.widget(find.byKey(AlertThresholdsPercentageDialogState.okButtonKey)).enabled, isFalse); }); }); @@ -230,7 +231,7 @@ void main() { testWidgets('never - closes dialog, returns threshold with value of -1', (tester) async { AlertThreshold initial = _mockThreshold(type: AlertType.courseGradeLow, value: '42'); AlertThreshold response = initial.rebuild((b) => b.threshold = '-1'); - AlertThreshold result; + AlertThreshold? result; var interactor = MockAlertThresholdsInteractor(); when(interactor.updateAlertThreshold(any, any, any, value: anyNamed('value'))) @@ -240,21 +241,22 @@ void main() { var widget = TestApp(Builder( builder: (context) => Container( - child: RaisedButton(onPressed: () async { + child: ElevatedButton(onPressed: () async { result = await showDialog( context: context, builder:(_) => AlertThresholdsPercentageDialog([initial], AlertType.courseGradeLow, '')); - }), - ))); + }, + child: Container(), + )))); // Show the dialog await tester.pumpWidget(widget); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Check to see if our initial value is there - expect(find.text(initial.threshold), findsOneWidget); + expect(find.text(initial.threshold!), findsOneWidget); // Tap on 'never' await tester.tap(find.text(AppLocalizations().never.toUpperCase())); @@ -269,16 +271,18 @@ void main() { // The dialog won't dismiss if it is the only child in the TestApp widget var widget = TestApp(Builder( builder: (context) => Container( - child: RaisedButton(onPressed: () async { - showDialog( - context: context, builder:(_) => AlertThresholdsPercentageDialog([], AlertType.courseGradeLow, '')); - }), + child: ElevatedButton( + child: Container(), + onPressed: () async { + showDialog( + context: context, builder:(_) => AlertThresholdsPercentageDialog([], AlertType.courseGradeLow, '')); + }), ))); // Show the dialog await tester.pumpWidget(widget); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Tap cancel @@ -352,16 +356,14 @@ void main() { }); } -class MockAlertThresholdsInteractor extends Mock implements AlertThresholdsInteractor {} - -void _setupLocator({AlertThresholdsInteractor thresholdsInteractor}) async { +void _setupLocator({AlertThresholdsInteractor? thresholdsInteractor}) async { var locator = GetIt.instance; await locator.reset(); locator.registerFactory(() => thresholdsInteractor ?? MockAlertThresholdsInteractor()); } -AlertThreshold _mockThreshold({AlertType type, String value}) => AlertThreshold((b) => b +AlertThreshold _mockThreshold({AlertType? type, String? value}) => AlertThreshold((b) => b ..alertType = type ?? AlertType.courseGradeLow ..threshold = value ?? null ..build()); diff --git a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart index 7204cc4f5e..141cd83e3e 100644 --- a/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart +++ b/apps/flutter_parent/test/screens/alert_thresholds/alert_thresholds_screen_test.dart @@ -32,12 +32,13 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/canvas_model_utils.dart'; import '../../utils/network_image_response.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { // For user images mockNetworkImageResponse(); - final interactor = _MockAlertThresholdsInteractor(); + final interactor = MockAlertThresholdsInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); @@ -240,7 +241,7 @@ void main() { TestApp( Builder(builder: (context) { return Material( - child: FlatButton( + child: TextButton( onPressed: () async { popValue = await QuickNav().push( context, @@ -254,7 +255,7 @@ void main() { ), ); await tester.pumpAndSettle(); - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // Tap overflow menu @@ -312,7 +313,7 @@ void main() { await tester.pump(); // Check to make sure we have the initial value - expect(find.text(NumberFormat.percentPattern().format(int.tryParse(initialValue) / 100)), findsOneWidget); + expect(find.text(NumberFormat.percentPattern().format(int.tryParse(initialValue)! / 100)), findsOneWidget); // Tap on a percent threshold await tester.tap(find.text(AppLocalizations().courseGradeBelow)); @@ -329,25 +330,25 @@ void main() { await tester.pumpAndSettle(); // Check for the update - expect(find.text(NumberFormat.percentPattern().format(int.tryParse(updatedValue) / 100)), findsOneWidget); + expect(find.text(NumberFormat.percentPattern().format(int.tryParse(updatedValue)! / 100)), findsOneWidget); }); }); } -Finder _percentageThresholdFinder(String title, {String value}) => find.byWidgetPredicate((widget) { +Finder _percentageThresholdFinder(String title, {String? value}) => find.byWidgetPredicate((widget) { return widget is ListTile && widget.title is Text && (widget.title as Text).data == title && widget.trailing is Text && (widget.trailing as Text).data == (value != null - ? NumberFormat.percentPattern().format(int.tryParse(value) / 100) + ? NumberFormat.percentPattern().format(int.tryParse(value)! / 100) : AppLocalizations().never) ? true : false; }); -Finder _switchThresholdFinder(String title, {bool switchedOn}) => find.byWidgetPredicate((widget) { +Finder _switchThresholdFinder(String title, {bool? switchedOn}) => find.byWidgetPredicate((widget) { return widget is SwitchListTile && widget.title is Text && (widget.title as Text).data == title && @@ -356,15 +357,14 @@ Finder _switchThresholdFinder(String title, {bool switchedOn}) => find.byWidgetP : false; }); -void _setupScreen(WidgetTester tester, [User student]) async { +Future _setupScreen(WidgetTester tester, [User? student]) async { var user = student ?? CanvasModelTestUtils.mockUser(); var screen = TestApp(AlertThresholdsScreen(user)); await tester.pumpWidget(screen); } -AlertThreshold _mockThreshold({AlertType type, String value}) => AlertThreshold((b) => b +AlertThreshold _mockThreshold({AlertType? type, String? value}) => AlertThreshold((b) => b ..alertType = type ?? AlertType.courseGradeLow ..threshold = value ?? null ..build()); -class _MockAlertThresholdsInteractor extends Mock implements AlertThresholdsInteractor {} diff --git a/apps/flutter_parent/test/screens/alerts/alerts_interactor_test.dart b/apps/flutter_parent/test/screens/alerts/alerts_interactor_test.dart index 00c6a93ff0..6c1ecf6abe 100644 --- a/apps/flutter_parent/test/screens/alerts/alerts_interactor_test.dart +++ b/apps/flutter_parent/test/screens/alerts/alerts_interactor_test.dart @@ -23,6 +23,7 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final alertId = '123'; @@ -73,7 +74,7 @@ void main() { final actual = await AlertsInteractor().getAlertsForStudent(studentId, false); verify(api.getAlertsDepaginated(studentId, false)).called(1); - expect(actual.alerts, data.reversed.toList()); // Verify that the actual list sorted correctly + expect(actual?.alerts, data.reversed.toList()); // Verify that the actual list sorted correctly }); test('get alerts for student returns thresholds', () async { @@ -86,7 +87,7 @@ void main() { verify(api.getAlertThresholds(studentId, false)).called(1); verifyNever(notifier.update(any)); // No call to notifier since we didn't force update - expect(actual.thresholds, data); // Verify that the actual list sorted correctly + expect(actual?.thresholds, data); // Verify that the actual list sorted correctly }); test('updates alert count notifier when forcing refresh', () async { diff --git a/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart b/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart index 74d62bc6e8..fed81ebfc1 100644 --- a/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart +++ b/apps/flutter_parent/test/screens/alerts/alerts_screen_test.dart @@ -40,15 +40,16 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; final _studentId = '123'; void main() { final String domain = 'https://test.instructure.com'; - final interactor = _MockAlertsInteractor(); - final announcementInteractor = _MockAnnouncementDetailsInteractor(); - final alertNotifier = _MockAlertCountNotifier(); - final mockNav = _MockNav(); + final interactor = MockAlertsInteractor(); + final announcementInteractor = MockAnnouncementDetailsInteractor(); + final alertNotifier = MockAlertCountNotifier(); + final mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); @@ -65,7 +66,7 @@ void main() { reset(mockNav); }); - void _pumpAndTapAlert(WidgetTester tester, Alert alert) async { + Future _pumpAndTapAlert(WidgetTester tester, Alert alert) async { final alerts = List.of([alert]); when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value(AlertsList(alerts, null))); @@ -84,7 +85,7 @@ void main() { group('Loading', () { testWidgetsWithAccessibilityChecks('Shows while waiting for future', (tester) async { - when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value()); + when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value(null)); await tester.pumpWidget(_testableWidget()); await tester.pump(); @@ -93,7 +94,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Does not show once loaded', (tester) async { - when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value()); + when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value(null)); await tester.pumpWidget(_testableWidget()); await tester.pump(); @@ -105,7 +106,7 @@ void main() { group('Empty message', () { testWidgetsWithAccessibilityChecks('Shows when response is null', (tester) async { - when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => null); + when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value(null)); await tester.pumpWidget(_testableWidget()); await tester.pumpAndSettle(); @@ -116,7 +117,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Shows when list is empty', (tester) async { - when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value(AlertsList(List(), null))); + when(interactor.getAlertsForStudent(_studentId, any)).thenAnswer((_) => Future.value(AlertsList([], null))); await tester.pumpWidget(_testableWidget()); await tester.pumpAndSettle(); @@ -157,7 +158,7 @@ void main() { testWidgetsWithAccessibilityChecks('refreshes when student changes', (tester) async { final notifier = SelectedStudentNotifier(); - when(interactor.getAlertsForStudent(any, any)).thenAnswer((_) => Future.value()); + when(interactor.getAlertsForStudent(any, any)).thenAnswer((_) => Future.value(null)); await tester.pumpWidget(_testableWidget(notifier: notifier)); await tester.pumpAndSettle(); @@ -165,8 +166,8 @@ void main() { verify(interactor.getAlertsForStudent(_studentId, any)).called(1); final newStudentId = _studentId + 'new'; - final newStudent = notifier.value.rebuild((b) => b..id = newStudentId); - notifier.update(newStudent); + final newStudent = notifier.value?.rebuild((b) => b..id = newStudentId); + notifier.update(newStudent!); await tester.pump(); verify(interactor.getAlertsForStudent(newStudentId, true)).called(1); @@ -182,9 +183,9 @@ void main() { final title = find.text(AppLocalizations().institutionAnnouncement); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, ParentColors.ash); + expect((tester.widget(title) as Text).style!.color, ParentColors.ash); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.info), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.info)) as Icon).color, ParentColors.ash); }); @@ -199,9 +200,9 @@ void main() { final title = find.text(AppLocalizations().courseAnnouncement); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, ParentColors.ash); + expect((tester.widget(title) as Text).style!.color, ParentColors.ash); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.info), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.info)) as Icon).color, ParentColors.ash); }); @@ -223,9 +224,9 @@ void main() { final title = find.text(AppLocalizations().courseGradeAboveThreshold(thresholdValue)); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, StudentColorSet.electric.light); + expect((tester.widget(title) as Text).style!.color, StudentColorSet.electric.light); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.info), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.info)) as Icon).color, StudentColorSet.electric.light); }); @@ -247,9 +248,9 @@ void main() { final title = find.text(AppLocalizations().assignmentGradeAboveThreshold(thresholdValue)); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, StudentColorSet.electric.light); + expect((tester.widget(title) as Text).style!.color, StudentColorSet.electric.light); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.info), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.info)) as Icon).color, StudentColorSet.electric.light); }); @@ -271,9 +272,9 @@ void main() { final title = find.text(AppLocalizations().courseGradeBelowThreshold(thresholdValue)); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, ParentColors.failure); + expect((tester.widget(title) as Text).style!.color, ParentColors.failure); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.warning), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.warning)) as Icon).color, ParentColors.failure); }); @@ -295,9 +296,9 @@ void main() { final title = find.text(AppLocalizations().assignmentGradeBelowThreshold(thresholdValue)); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, ParentColors.failure); + expect((tester.widget(title) as Text).style!.color, ParentColors.failure); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.warning), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.warning)) as Icon).color, ParentColors.failure); }); @@ -312,9 +313,9 @@ void main() { final title = find.text(AppLocalizations().assignmentMissing); expect(title, findsOneWidget); - expect((tester.widget(title) as Text).style.color, ParentColors.failure); + expect((tester.widget(title) as Text).style!.color, ParentColors.failure); expect(find.text(alerts.first.title), findsOneWidget); - expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(alerts.first.actionDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.warning), findsOneWidget); expect((tester.widget(find.byIcon(CanvasIcons.warning)) as Icon).color, ParentColors.failure); }); @@ -510,7 +511,7 @@ void main() { await tester.pumpWidget(_testableWidget()); await tester.pumpAndSettle(); - final semantics = find.bySemanticsLabel(AppLocalizations().dismissAlertLabel(alert.title)); + final semantics = find.byTooltip(AppLocalizations().dismissAlertLabel(alert.title)); final icon = find.byIcon(Icons.clear); expect(find.descendant(of: semantics, matching: icon), findsOneWidget); @@ -518,11 +519,11 @@ void main() { }); } -Widget _testableWidget({SelectedStudentNotifier notifier}) { +Widget _testableWidget({SelectedStudentNotifier? notifier}) { notifier = notifier ?? SelectedStudentNotifier(); return TestApp( ChangeNotifierProvider( - create: (context) => notifier..value = CanvasModelTestUtils.mockUser(id: _studentId, name: 'Trevor'), + create: (context) => notifier?..value = CanvasModelTestUtils.mockUser(id: _studentId, name: 'Trevor'), child: Consumer(builder: (context, model, _) { return Scaffold(body: AlertsScreen()); }), @@ -532,7 +533,7 @@ Widget _testableWidget({SelectedStudentNotifier notifier}) { } List _mockData( - {int size = 1, AlertType type, AlertWorkflowState state = AlertWorkflowState.read, String htmlUrl = ''}) { + {int size = 1, AlertType? type, AlertWorkflowState state = AlertWorkflowState.read, String htmlUrl = ''}) { return List.generate( size, (index) => Alert((b) => b @@ -542,12 +543,4 @@ List _mockData( ..alertType = type ?? AlertType.institutionAnnouncement ..htmlUrl = htmlUrl ..lockedForUser = false)); -} - -class _MockAlertsInteractor extends Mock implements AlertsInteractor {} - -class _MockAnnouncementDetailsInteractor extends Mock implements AnnouncementDetailsInteractor {} - -class _MockAlertCountNotifier extends Mock implements AlertCountNotifier {} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/announcements/announcement_details_interactor_test.dart b/apps/flutter_parent/test/screens/announcements/announcement_details_interactor_test.dart index e68314cace..80fdc82ecd 100644 --- a/apps/flutter_parent/test/screens/announcements/announcement_details_interactor_test.dart +++ b/apps/flutter_parent/test/screens/announcements/announcement_details_interactor_test.dart @@ -29,6 +29,7 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { //region data config @@ -75,8 +76,8 @@ void main() { //endregion - final announcementApi = _MockAnnouncementApi(); - final courseApi = _MockCourseApi(); + final announcementApi = MockAnnouncementApi(); + final courseApi = MockCourseApi(); setupTestLocator((locator) { locator.registerFactory(() => announcementApi); @@ -103,11 +104,11 @@ void main() { verify(announcementApi.getCourseAnnouncement(course.id, announcement.id, true)).called(1); verify(courseApi.getCourse(course.id)).called(1); - expect(actualViewState.toolbarTitle, expectedViewState.toolbarTitle); - expect(actualViewState.announcementMessage, expectedViewState.announcementMessage); - expect(actualViewState.announcementTitle, expectedViewState.announcementTitle); - expect(actualViewState.postedAt, expectedViewState.postedAt); - expect(actualViewState.attachment, attachment); + expect(actualViewState?.toolbarTitle, expectedViewState.toolbarTitle); + expect(actualViewState?.announcementMessage, expectedViewState.announcementMessage); + expect(actualViewState?.announcementTitle, expectedViewState.announcementTitle); + expect(actualViewState?.postedAt, expectedViewState.postedAt); + expect(actualViewState?.attachment, attachment); }); test('get course announcement returns a proper view state with no attachments', () async { @@ -125,11 +126,11 @@ void main() { verify(announcementApi.getCourseAnnouncement(course.id, announcement.id, true)).called(1); verify(courseApi.getCourse(course.id)).called(1); - expect(actualViewState.toolbarTitle, expectedViewState.toolbarTitle); - expect(actualViewState.announcementMessage, expectedViewState.announcementMessage); - expect(actualViewState.announcementTitle, expectedViewState.announcementTitle); - expect(actualViewState.postedAt, expectedViewState.postedAt); - expect(actualViewState.attachment, expectedViewState.attachment); + expect(actualViewState?.toolbarTitle, expectedViewState.toolbarTitle); + expect(actualViewState?.announcementMessage, expectedViewState.announcementMessage); + expect(actualViewState?.announcementTitle, expectedViewState.announcementTitle); + expect(actualViewState?.postedAt, expectedViewState.postedAt); + expect(actualViewState?.attachment, expectedViewState.attachment); }); test('get institution announcement returns a proper view state', () async { @@ -146,13 +147,9 @@ void main() { verify(announcementApi.getAccountNotification(accountNotification.id, true)).called(1); - expect(actualViewState.toolbarTitle, expectedViewState.toolbarTitle); - expect(actualViewState.announcementMessage, expectedViewState.announcementMessage); - expect(actualViewState.announcementTitle, expectedViewState.announcementTitle); - expect(actualViewState.postedAt, expectedViewState.postedAt); + expect(actualViewState?.toolbarTitle, expectedViewState.toolbarTitle); + expect(actualViewState?.announcementMessage, expectedViewState.announcementMessage); + expect(actualViewState?.announcementTitle, expectedViewState.announcementTitle); + expect(actualViewState?.postedAt, expectedViewState.postedAt); }); -} - -class _MockAnnouncementApi extends Mock implements AnnouncementApi {} - -class _MockCourseApi extends Mock implements CourseApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart b/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart index 8806783987..17289b1b87 100644 --- a/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart +++ b/apps/flutter_parent/test/screens/announcements/announcement_details_screen_test.dart @@ -31,9 +31,10 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - final interactor = _MockAnnouncementDetailsInteractor(); + final interactor = MockAnnouncementDetailsInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); @@ -46,7 +47,7 @@ void main() { group('Loading', () { testWidgetsWithAccessibilityChecks('Shows while waiting for future', (tester) async { - when(interactor.getAnnouncement(any, any, any, any, any)).thenAnswer((_) => Future.value()); + when(interactor.getAnnouncement(any, any, any, any, any)).thenAnswer((_) => Future.value(null)); await tester.pumpWidget(_testableWidget('', AnnouncementType.COURSE, '')); await tester.pump(); @@ -55,7 +56,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Does not show once loaded', (tester) async { - when(interactor.getAnnouncement(any, any, any, any, any)).thenAnswer((_) => Future.value()); + when(interactor.getAnnouncement(any, any, any, any, any)).thenAnswer((_) => Future.value(null)); await tester.pumpWidget(_testableWidget('', AnnouncementType.COURSE, '')); await tester.pump(); @@ -126,7 +127,7 @@ void main() { expect(find.text(announcementSubject), findsOneWidget); expect(find.text(courseName), findsOneWidget); - expect(find.text(postedAt.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(postedAt.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byType(HtmlDescriptionTile), findsOneWidget); }); @@ -152,7 +153,7 @@ void main() { expect(find.text(announcementSubject), findsOneWidget); expect(find.text(courseName), findsOneWidget); - expect(find.text(postedAt.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(postedAt.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byType(HtmlDescriptionTile), findsOneWidget); var attachmentWidget = find.byType(AttachmentIndicatorWidget); expect(attachmentWidget, findsOneWidget); @@ -177,7 +178,7 @@ void main() { expect(find.text(announcementSubject), findsOneWidget); expect(find.text(toolbarTitle), findsOneWidget); - expect(find.text(postedAt.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(postedAt.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.byType(HtmlDescriptionTile), findsOneWidget); }); }); @@ -192,5 +193,3 @@ Widget _testableWidget(String announcementId, AnnouncementType type, String cour ), ); } - -class _MockAnnouncementDetailsInteractor extends Mock implements AnnouncementDetailsInteractor {} diff --git a/apps/flutter_parent/test/screens/assignments/assignment_details_interactor_test.dart b/apps/flutter_parent/test/screens/assignments/assignment_details_interactor_test.dart index a370412952..552cd13dbc 100644 --- a/apps/flutter_parent/test/screens/assignments/assignment_details_interactor_test.dart +++ b/apps/flutter_parent/test/screens/assignments/assignment_details_interactor_test.dart @@ -28,6 +28,7 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final assignmentId = '123'; @@ -66,7 +67,7 @@ void main() { final details = await AssignmentDetailsInteractor().loadAssignmentDetails(false, courseId, assignmentId, studentId); - expect(details.course, course); + expect(details?.course, course); }); test('loadReminder calls ReminderDb', () async { @@ -164,7 +165,7 @@ void main() { final details = await AssignmentDetailsInteractor().loadAssignmentDetails(false, courseId, assignmentId, studentId); - expect(details.assignment, assignment); + expect(details?.assignment, assignment); }); }); } diff --git a/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart b/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart index af63dbe49d..89ff60074a 100644 --- a/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart +++ b/apps/flutter_parent/test/screens/assignments/assignment_details_screen_test.dart @@ -47,6 +47,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final courseId = '123'; @@ -153,33 +154,35 @@ void main() { }); testWidgetsWithAccessibilityChecks('Can send a message', (tester) async { - when(convoInteractor.loadData(any, any)).thenAnswer((_) async => CreateConversationData(Course(), [])); - when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)) - .thenAnswer((_) async => AssignmentDetails(assignment: assignment)); - - await tester.pumpWidget(TestApp( - AssignmentDetailsScreen( - courseId: courseId, - assignmentId: assignmentId, - ), - platformConfig: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}), - )); - - await tester.pumpAndSettle(); - - await tester.tap(find.byType(FloatingActionButton)); - await tester.pumpAndSettle(); - - // Check to make sure we're on the conversation screen - expect(find.byType(CreateConversationScreen), findsOneWidget); - - // Check that we have the correct subject line - expect(find.text(AppLocalizations().assignmentSubjectMessage(studentName, assignmentName)), findsOneWidget); + await tester.runAsync(() async { + when(convoInteractor.loadData(any, any)).thenAnswer((_) async => CreateConversationData(Course(), [])); + when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)) + .thenAnswer((_) async => AssignmentDetails(assignment: assignment)); + + await tester.pumpWidget(TestApp( + AssignmentDetailsScreen( + courseId: courseId, + assignmentId: assignmentId, + ), + platformConfig: PlatformConfig(mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}), + )); + + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + // Check to make sure we're on the conversation screen + expect(find.byType(CreateConversationScreen), findsOneWidget); + + // Check that we have the correct subject line + expect(find.text(AppLocalizations().assignmentSubjectMessage(studentName, assignmentName)), findsOneWidget); + }); }); testWidgetsWithAccessibilityChecks('shows error', (tester) async { when(interactor.loadAssignmentDetails(any, courseId, assignmentId, studentId)) - .thenAnswer((_) => Future.error('Failed to get assignment')); + .thenAnswer((_) => Future.error('Failed to get assignment')); await tester.pumpWidget(TestApp( AssignmentDetailsScreen( @@ -224,13 +227,13 @@ void main() { await tester.pumpAndSettle(Duration(seconds: 1)); expect(find.text(assignmentName), findsOneWidget); - expect(find.text(dueDate.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(dueDate.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); expect(find.text('1.5 pts'), findsOneWidget); expect(find.byIcon(Icons.do_not_disturb), findsOneWidget); - expect((tester.widget(find.byIcon(Icons.do_not_disturb)) as Icon).color, ParentColors.ash); + expect((tester.widget(find.byIcon(Icons.do_not_disturb)) as Icon).color, ParentColors.oxford); expect(find.text(AppLocalizations().assignmentNotSubmittedLabel), findsOneWidget); - expect((tester.widget(find.text(AppLocalizations().assignmentNotSubmittedLabel)) as Text).style.color, - ParentColors.ash); + expect((tester.widget(find.text(AppLocalizations().assignmentNotSubmittedLabel)) as Text).style!.color, + ParentColors.oxford); expect(find.text(AppLocalizations().assignmentRemindMeDescription), findsOneWidget); expect((tester.widget(find.byType(Switch)) as Switch).value, false); expect(find.text(AppLocalizations().descriptionTitle), findsOneWidget); @@ -278,7 +281,7 @@ void main() { expect((iconContainer.decoration as BoxDecoration).color, StudentColorSet.shamrock.light); expect(find.text(AppLocalizations().assignmentSubmittedLabel), findsOneWidget); - expect((tester.widget(find.text(AppLocalizations().assignmentSubmittedLabel)) as Text).style.color, + expect((tester.widget(find.text(AppLocalizations().assignmentSubmittedLabel)) as Text).style!.color, StudentColorSet.shamrock.light); }); @@ -499,7 +502,7 @@ void main() { expect(find.text(AppLocalizations().assignmentRemindMeSet), findsOneWidget); expect((tester.widget(find.byType(Switch)) as Switch).value, true); - expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); }); testWidgetsWithAccessibilityChecks('creates reminder without due date', (tester) async { @@ -539,7 +542,7 @@ void main() { expect(find.text(AppLocalizations().assignmentRemindMeSet), findsOneWidget); expect((tester.widget(find.byType(Switch)) as Switch).value, true); - expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); }); testWidgetsWithAccessibilityChecks('creates reminder with due date', (tester) async { @@ -581,7 +584,7 @@ void main() { expect(find.text(AppLocalizations().assignmentRemindMeSet), findsOneWidget); expect((tester.widget(find.byType(Switch)) as Switch).value, true); - expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); }); testWidgetsWithAccessibilityChecks('deletes reminder', (tester) async { diff --git a/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart b/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart index 32f6503b76..134975c7df 100644 --- a/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart +++ b/apps/flutter_parent/test/screens/assignments/grade_cell_data_test.dart @@ -26,14 +26,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.dart'; void main() { - Assignment baseAssignment; - Submission baseSubmission; - GradeCellData baseGradedState; - Course baseCourse; + late Assignment baseAssignment; + late Submission baseSubmission; + late GradeCellData baseGradedState; + late Course baseCourse; - Color accentColor = Colors.pinkAccent; + Color secondaryColor = Colors.pinkAccent; - ThemeData theme = ThemeData(accentColor: accentColor); + ThemeData theme = ThemeData().copyWith(colorScheme: ThemeData().colorScheme.copyWith(secondary: secondaryColor)); AppLocalizations l10n = AppLocalizations(); setUp(() { @@ -57,7 +57,7 @@ void main() { baseGradedState = GradeCellData((b) => b ..state = GradeCellState.graded - ..accentColor = accentColor + ..accentColor = secondaryColor ..outOf = 'Out of 100 points'); final gradingSchemeBuilder = ListBuilder() diff --git a/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart b/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart index 4e42d23b10..37bd688ec3 100644 --- a/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart +++ b/apps/flutter_parent/test/screens/assignments/grade_cell_widget_test.dart @@ -162,7 +162,7 @@ void main() { await setupWithData(tester, data); expect(find.byKey(Key('grade-cell-late-penalty')).evaluate(), find.text(data.latePenalty).evaluate()); - expect(tester.widget(find.byKey(Key('grade-cell-late-penalty'))).style.color, data.accentColor); + expect(tester.widget(find.byKey(Key('grade-cell-late-penalty'))).style!.color, data.accentColor); }); testWidgetsWithAccessibilityChecks('Displays final grade text', (tester) async { diff --git a/apps/flutter_parent/test/screens/aup/acceptable_use_policy_screen_test.dart b/apps/flutter_parent/test/screens/aup/acceptable_use_policy_screen_test.dart index aa39a4856d..42c264aec2 100644 --- a/apps/flutter_parent/test/screens/aup/acceptable_use_policy_screen_test.dart +++ b/apps/flutter_parent/test/screens/aup/acceptable_use_policy_screen_test.dart @@ -23,12 +23,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { testWidgets('Submit button disabled when switch is not checked', (tester) async { - var interactor = _MockInteractor(); - var nav = _MockNav(); + var interactor = MockAcceptableUsePolicyInteractor(); + var nav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => nav); @@ -43,8 +44,8 @@ void main() { }); testWidgets('Submit button enabled when switch is checked', (tester) async { - var interactor = _MockInteractor(); - var nav = _MockNav(); + var interactor = MockAcceptableUsePolicyInteractor(); + var nav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => nav); @@ -63,11 +64,11 @@ void main() { }); testWidgets('Submit button navigates to splash screen', (tester) async { - var interactor = _MockInteractor(); - var mockNav = _MockNav(); + var interactor = MockAcceptableUsePolicyInteractor(); + var nav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); - locator.registerLazySingleton(() => mockNav); + locator.registerLazySingleton(() => nav); }); when(interactor.acceptTermsOfUse()).thenAnswer((_) async => User()); @@ -84,10 +85,6 @@ void main() { await tester.pump(); - verify(mockNav.pushRouteAndClearStack(any, '/')); + verify(nav.pushRouteAndClearStack(any, '/')); }); -} - -class _MockInteractor extends Mock implements AcceptableUsePolicyInteractor {} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/calendar/calendar_day_list_tile_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_day_list_tile_test.dart index 5f9ccd4156..78f3aab73b 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_day_list_tile_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_day_list_tile_test.dart @@ -23,6 +23,7 @@ import 'package:flutter_parent/models/planner_submission.dart'; import 'package:flutter_parent/models/serializers.dart'; import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; +import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/assignments/assignment_details_interactor.dart'; import 'package:flutter_parent/screens/assignments/assignment_details_screen.dart'; import 'package:flutter_parent/screens/calendar/calendar_day_list_tile.dart'; @@ -31,11 +32,13 @@ import 'package:flutter_parent/screens/events/event_details_screen.dart'; import 'package:flutter_parent/utils/design/canvas_icons.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final studentId = '1337'; @@ -121,8 +124,8 @@ void main() { expect(find.byWidgetPredicate((widget) { if (widget is Text) { final Text textWidget = widget; - if (textWidget.data != null) return textWidget.data.contains('pts'); - return textWidget.textSpan.toPlainText().contains('pts'); + if (textWidget.data != null) return textWidget.data!.contains('pts'); + return textWidget.textSpan!.toPlainText().contains('pts'); } return false; }), findsNothing); @@ -391,20 +394,20 @@ void main() { }); } -Plannable _createPlannable({String title, DateTime dueAt, double pointsPossible, String assignmentId}) => +Plannable _createPlannable({String? title, DateTime? dueAt, double? pointsPossible, String? assignmentId}) => Plannable((b) => b ..id = '' ..title = title ?? '' ..pointsPossible = pointsPossible ..dueAt = dueAt - ..assignmentId = assignmentId); + ..assignmentId = assignmentId ?? ''); PlannerItem _createPlannerItem( - {String contextName, - Plannable plannable, - String plannableType, - PlannerSubmission submission, - String htmlUrl}) => + {String? contextName, + Plannable? plannable, + String? plannableType, + PlannerSubmission? submission, + String? htmlUrl}) => PlannerItem((b) => b ..courseId = '' ..plannable = plannable != null ? plannable.toBuilder() : _createPlannable().toBuilder() diff --git a/apps/flutter_parent/test/screens/calendar/calendar_day_planner_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_day_planner_test.dart index e1a3845cf7..2b924d82bd 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_day_planner_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_day_planner_test.dart @@ -36,6 +36,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/canvas_model_utils.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import '../courses/course_summary_screen_test.dart'; void main() { @@ -53,8 +54,8 @@ void main() { ..enrollmentState = 'active') ]).toBuilder()); - final CalendarEventsApi api = MockCalendarApi(); - final CoursesInteractor interactor = MockCoursesInteractor(); + final MockCalendarEventsApi api = MockCalendarEventsApi(); + final MockCoursesInteractor interactor = MockCoursesInteractor(); setupTestLocator((locator) { locator.registerLazySingleton(() => api); @@ -103,7 +104,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('shows error panda view when we fail to retrieve events', (tester) async { - Completer completer = Completer>(); + Completer> completer = Completer>(); when(api.getUserCalendarItems(any, any, any, ScheduleItem.apiTypeAssignment, contexts: anyNamed('contexts'), forceRefresh: anyNamed('forceRefresh'))) .thenAnswer((_) => completer.future); @@ -179,7 +180,7 @@ void main() { }); } -ScheduleItem _createScheduleItem({String contextName, String type = ScheduleItem.apiTypeAssignment}) => +ScheduleItem _createScheduleItem({String? contextName, String type = ScheduleItem.apiTypeAssignment}) => ScheduleItem((b) => b ..id = '' ..title = '' diff --git a/apps/flutter_parent/test/screens/calendar/calendar_screen_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_screen_test.dart index bc07e310ce..95401597de 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_screen_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_screen_test.dart @@ -45,12 +45,13 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - CalendarEventsApi calendarApi = MockCalendarApi(); - CalendarFilterDb filterDb = MockCalendarFilterDb(); - CalendarFilterListInteractor filterInteractor = MockCalendarFilterListInteractor(); - CoursesInteractor coursesInteractor = MockCoursesInteractor(); + MockCalendarEventsApi calendarApi = MockCalendarEventsApi(); + MockCalendarFilterDb filterDb = MockCalendarFilterDb(); + MockCalendarFilterListInteractor filterInteractor = MockCalendarFilterListInteractor(); + MockCoursesInteractor coursesInteractor = MockCoursesInteractor(); when(filterDb.getByObserveeId(any, any, any)) .thenAnswer((_) => Future.value(CalendarFilter((b) => b.filters = SetBuilder({'course_123'})))); @@ -120,6 +121,7 @@ void main() { await tester.tap(find.text(AppLocalizations().calendars)); await tester.pump(); await tester.pump(); + await tester.pumpAndSettle(); // Check for the filter screen expect(find.byType(CalendarFilterListScreen), findsOneWidget); @@ -308,7 +310,7 @@ void main() { */ } -Widget _testableMaterialWidget({Widget widget, SelectedStudentNotifier notifier = null, NavigatorObserver observer}) { +Widget _testableMaterialWidget({Widget? widget, SelectedStudentNotifier? notifier = null, NavigatorObserver? observer}) { var login = Login((b) => b ..uuid = 'uuid' ..domain = 'domain' diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_of_week_headers_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_of_week_headers_test.dart index 091c37371e..24ef664b03 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_of_week_headers_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_of_week_headers_test.dart @@ -61,14 +61,14 @@ void main() { // Week days should use dark text weekdays.forEach((day) { - final textColor = tester.widget(day).style.color; + final textColor = tester.widget(day).style!.color; expect(textColor, ParentColors.licorice); }); // weekends should use faded text weekends.forEach((day) { - final textColor = tester.widget(day).style.color; - expect(textColor, ParentColors.ash); + final textColor = tester.widget(day).style!.color; + expect(textColor, ParentColors.oxford); }); }); @@ -112,14 +112,14 @@ void main() { // Week days should use dark text weekdays.forEach((day) { - final textColor = tester.widget(day).style.color; + final textColor = tester.widget(day).style!.color; expect(textColor, ParentColors.licorice); }); // weekends should use faded text weekends.forEach((day) { - final textColor = tester.widget(day).style.color; - expect(textColor, ParentColors.ash); + final textColor = tester.widget(day).style!.color; + expect(textColor, ParentColors.oxford); }); }); @@ -163,14 +163,14 @@ void main() { // Week days should use dark text weekdays.forEach((day) { - final textColor = tester.widget(day).style.color; + final textColor = tester.widget(day).style!.color; expect(textColor, ParentColors.licorice); }); // weekends should use faded text weekends.forEach((day) { - final textColor = tester.widget(day).style.color; - expect(textColor, ParentColors.ash); + final textColor = tester.widget(day).style!.color; + expect(textColor, ParentColors.oxford); }); }); } diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_test.dart index 839148dffc..550eaeb124 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_day_test.dart @@ -109,12 +109,12 @@ void main() { final theme = Theme.of(tester.element(find.byType(CalendarDay))); final textStyle = tester.widget(find.byType(AnimatedDefaultTextStyle).last).style; - expect(textStyle.color, theme.accentColor); + expect(textStyle.color, theme.colorScheme.secondary); final container = find.ancestor(of: find.text('2'), matching: find.byType(Container)).first; final decoration = tester.widget(container).decoration as BoxDecoration; expect(decoration.borderRadius, BorderRadius.circular(16)); - expect(decoration.border, Border.all(color: theme.accentColor, width: 2)); + expect(decoration.border, Border.all(color: theme.colorScheme.secondary, width: 2)); }); testWidgetsWithAccessibilityChecks('Uses white text color and accent background for today', (tester) async { @@ -132,11 +132,15 @@ void main() { final container = find.ancestor(of: find.text(date.day.toString()), matching: find.byType(Container)).first; final decoration = tester.widget(container).decoration; - expect(decoration, BoxDecoration(color: theme.accentColor, shape: BoxShape.circle)); + expect(decoration, BoxDecoration(color: theme.colorScheme.secondary, shape: BoxShape.circle)); }); testWidgetsWithAccessibilityChecks('Displays activity dots', (tester) async { - final fetcher = _FakeFetcher(); + final fetcher = _FakeFetcher( + observeeId: '', + userDomain: '', + userId: '', + ); fetcher.nextSnapshot = AsyncSnapshot>.withData(ConnectionState.done, [ _createPlannerItem(contextName: 'blank'), _createPlannerItem(contextName: 'blank'), @@ -169,7 +173,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Invokes onDaySelectedCallback', (tester) async { - DateTime selection = null; + DateTime? selection = null; await tester.pumpWidget(_appWithFetcher(CalendarDay( date: dayDate, selectedDay: selectedDate, @@ -186,10 +190,14 @@ void main() { }); } -Widget _appWithFetcher(Widget child, {PlannerFetcher fetcher}) { +Widget _appWithFetcher(Widget child, {PlannerFetcher? fetcher}) { return TestApp( ChangeNotifierProvider( - create: (BuildContext context) => fetcher ?? _FakeFetcher(), + create: (BuildContext context) => fetcher ?? _FakeFetcher( + observeeId: '', + userDomain: '', + userId: '', + ), child: child, ), ); @@ -198,6 +206,8 @@ Widget _appWithFetcher(Widget child, {PlannerFetcher fetcher}) { class _FakeFetcher extends PlannerFetcher { AsyncSnapshot> nextSnapshot = AsyncSnapshot>.withData(ConnectionState.done, []); + _FakeFetcher({required super.observeeId, required super.userDomain, required super.userId}); + @override AsyncSnapshot> getSnapshotForDate(DateTime date) => nextSnapshot; } @@ -206,7 +216,7 @@ Plannable _createPlannable() => Plannable((b) => b ..id = '' ..title = ''); -PlannerItem _createPlannerItem({String contextName}) => PlannerItem((b) => b +PlannerItem _createPlannerItem({String? contextName}) => PlannerItem((b) => b ..courseId = '' ..plannable = _createPlannable().toBuilder() ..contextType = '' diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_interactor_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_interactor_test.dart index 972bfd8b09..358d1c4a9b 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_interactor_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_interactor_test.dart @@ -28,6 +28,7 @@ import '../../../utils/canvas_model_utils.dart'; import '../../../utils/platform_config.dart'; import '../../../utils/test_app.dart'; import '../../../utils/test_helpers/mock_helpers.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final _api = MockCourseApi(); diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_screen_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_screen_test.dart index 62bdea8c5a..5ae455a666 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_screen_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_filter_list_screen_test.dart @@ -28,6 +28,7 @@ import 'package:mockito/mockito.dart'; import '../../../utils/accessibility_utils.dart'; import '../../../utils/test_app.dart'; import '../../../utils/test_helpers/mock_helpers.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { group('Render', () { @@ -161,7 +162,7 @@ void main() { expect(find.byWidgetPredicate((widget) { if (widget is Checkbox) { final Checkbox checkboxWidget = widget; - return checkboxWidget.value; + return checkboxWidget.value!; } return false; }), findsNWidgets(3)); @@ -184,7 +185,7 @@ void main() { expect(find.byWidgetPredicate((widget) { if (widget is Checkbox) { final Checkbox checkboxWidget = widget; - return checkboxWidget.value; + return checkboxWidget.value!; } return false; }), findsNWidgets(1)); @@ -213,7 +214,7 @@ void main() { if (widget is Checkbox) { // Check for a checkbox widgets that are checked final Checkbox checkboxWidget = widget; - return checkboxWidget.value; + return checkboxWidget.value!; } return false; }); @@ -311,7 +312,7 @@ void main() { if (widget is Checkbox) { // Check for a checkbox widgets that are checked final Checkbox checkboxWidget = widget; - return checkboxWidget.value; + return checkboxWidget.value!; } return false; }); @@ -355,7 +356,7 @@ void main() { if (widget is Checkbox) { // Check for a checkbox widgets that are checked final Checkbox checkboxWidget = widget; - return checkboxWidget.value; + return checkboxWidget.value!; } return false; }, skipOffstage: false); diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_month_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_month_test.dart index 35396c012e..9792f8e3ca 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_month_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_month_test.dart @@ -118,7 +118,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Invokes onDaySelected callback', (tester) async { - DateTime selected = null; + DateTime? selected = null; await tester.pumpWidget( _appWithFetcher( @@ -229,10 +229,14 @@ void main() { }); } -Widget _appWithFetcher(Widget child, {PlannerFetcher fetcher}) { +Widget _appWithFetcher(Widget child, {PlannerFetcher? fetcher}) { return TestApp( ChangeNotifierProvider( - create: (BuildContext context) => fetcher ?? _FakeFetcher(), + create: (BuildContext context) => fetcher ?? _FakeFetcher( + observeeId: '', + userDomain: '', + userId: '', + ), child: child, ), ); @@ -241,6 +245,8 @@ Widget _appWithFetcher(Widget child, {PlannerFetcher fetcher}) { class _FakeFetcher extends PlannerFetcher { AsyncSnapshot> nextSnapshot = AsyncSnapshot>.withData(ConnectionState.done, []); + _FakeFetcher({required super.observeeId, required super.userDomain, required super.userId}); + @override AsyncSnapshot> getSnapshotForDate(DateTime date) => nextSnapshot; } diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_week_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_week_test.dart index f949978aba..910268d062 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_week_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_week_test.dart @@ -71,7 +71,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Invokes onDaySelected callback', (tester) async { - DateTime selected = null; + DateTime? selected = null; await tester.pumpWidget( _appWithFetcher( CalendarWeek( @@ -168,7 +168,7 @@ void main() { }).toList(); // We expect X positions to be in ascending order - final List expectedCenters = List.from(centers).sortBy([(it) => it]); + final List? expectedCenters = List.from(centers).sortBySelector([(it) => it]); expect(centers, expectedCenters); }); @@ -195,16 +195,20 @@ void main() { }).toList(); // We expect X positions to be in descending order - final List expectedCenters = List.from(centers).sortBy([(it) => it], descending: true); + final List? expectedCenters = List.from(centers).sortBySelector([(it) => it], descending: true); expect(centers, expectedCenters); }); } -Widget _appWithFetcher(Widget child, {PlannerFetcher fetcher, Locale locale}) { +Widget _appWithFetcher(Widget child, {PlannerFetcher? fetcher, Locale? locale}) { return TestApp( ChangeNotifierProvider( - create: (BuildContext context) => fetcher ?? _FakeFetcher(), + create: (BuildContext context) => fetcher ?? _FakeFetcher( + observeeId: '', + userDomain: '', + userId: '', + ), child: child, ), locale: locale, @@ -214,6 +218,8 @@ Widget _appWithFetcher(Widget child, {PlannerFetcher fetcher, Locale locale}) { class _FakeFetcher extends PlannerFetcher { AsyncSnapshot> nextSnapshot = AsyncSnapshot>.withData(ConnectionState.done, []); + _FakeFetcher({required super.observeeId, required super.userDomain, required super.userId}); + @override AsyncSnapshot> getSnapshotForDate(DateTime date) => nextSnapshot; } diff --git a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_widget_test.dart b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_widget_test.dart index dfec420b83..f171afaa9d 100644 --- a/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_widget_test.dart +++ b/apps/flutter_parent/test/screens/calendar/calendar_widget/calendar_widget_test.dart @@ -44,7 +44,7 @@ void main() { return state; } - Widget calendarTestApp(Widget child, {Locale locale}) { + Widget calendarTestApp(Widget child, {Locale? locale}) { return TestApp(ChangeNotifierProvider(create: (_) => CalendarTodayNotifier(), child: child), locale: locale); } @@ -83,7 +83,7 @@ void main() { testWidgetsWithAccessibilityChecks('Switches to specified starting date', (tester) async { DateTime startingDate = DateTime(2000, 1, 1); - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; await tester.pumpWidget( calendarTestApp( CalendarWidget( @@ -115,7 +115,7 @@ void main() { var pressed = false; final calendar = CalendarWidget( - dayBuilder: (_, __) => FlatButton( + dayBuilder: (_, __) => TextButton( onPressed: () => pressed = true, child: Text('Press me!'), ), @@ -137,7 +137,7 @@ void main() { expect(find.byType(CalendarMonth), findsOneWidget); // Tap on the 'event' - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // Should have our pressed value true @@ -148,7 +148,7 @@ void main() { var pressed = false; final calendar = CalendarWidget( - dayBuilder: (_, __) => FlatButton( + dayBuilder: (_, __) => TextButton( onPressed: () => pressed = true, child: Text('Press me!'), ), @@ -167,7 +167,7 @@ void main() { expect(find.byType(CalendarWeek), findsOneWidget); // Tap on the 'event' - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // Should have our pressed value true @@ -294,7 +294,7 @@ void main() { group('Set day/week/month', () { testWidgetsWithAccessibilityChecks('Jumps to selected date', (tester) async { - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; await tester.pumpWidget( calendarTestApp( CalendarWidget( @@ -325,7 +325,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Animates to selected date', (tester) async { - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; await tester.pumpWidget( calendarTestApp( CalendarWidget( @@ -687,7 +687,7 @@ void main() { group('Date Selection', () { testWidgetsWithAccessibilityChecks('Selects date from week view', (tester) async { - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; await tester.pumpWidget( calendarTestApp( CalendarWidget( @@ -720,7 +720,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Selects date from month view', (tester) async { - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; await tester.pumpWidget( calendarTestApp( CalendarWidget( @@ -756,7 +756,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Swipes to select adjacent day', (tester) async { - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; final dayContentKey = Key('day-content'); await tester.pumpWidget( @@ -855,13 +855,13 @@ void main() { // there will be at least one build pass where the month layout overflows its parent. This is acceptable while the // month view is animating its collapse into the week view. However, because such overflows will fail the test, // we must intercept and ignore those specific errors - FlutterExceptionHandler onError = FlutterError.onError; + FlutterExceptionHandler? onError = FlutterError.onError; FlutterError.onError = (details) { var exception = details.exception; - if (exception is FlutterError && exception?.message?.startsWith('A RenderFlex overflowed') == true) { + if (exception is FlutterError && exception.message.startsWith('A RenderFlex overflowed')) { // Intentionally left blank } else { - onError(details); + onError!(details); } }; @@ -874,7 +874,7 @@ void main() { fetcher: _FakeFetcher(), ); - StateSetter stateSetter; + late StateSetter stateSetter; await tester.pumpWidget( calendarTestApp( @@ -1064,7 +1064,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Swipes to select adjacent day in RTL', (tester) async { - DateTime dateForDayBuilder = null; + DateTime? dateForDayBuilder = null; final dayContentKey = Key('day-content'); await tester.pumpWidget( @@ -1105,6 +1105,8 @@ void main() { class _FakeFetcher extends PlannerFetcher { AsyncSnapshot> nextSnapshot = AsyncSnapshot>.withData(ConnectionState.done, []); + _FakeFetcher({super.observeeId = '', super.userDomain = '', super.userId = ''}); + @override AsyncSnapshot> getSnapshotForDate(DateTime date) => nextSnapshot; } diff --git a/apps/flutter_parent/test/screens/calendar/planner_fetcher_test.dart b/apps/flutter_parent/test/screens/calendar/planner_fetcher_test.dart index 1ab3f5305d..d4cb21e37b 100644 --- a/apps/flutter_parent/test/screens/calendar/planner_fetcher_test.dart +++ b/apps/flutter_parent/test/screens/calendar/planner_fetcher_test.dart @@ -29,11 +29,12 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - CalendarEventsApi api = MockCalendarApi(); - CalendarFilterDb filterDb = MockCalendarFilterDb(); - CoursesInteractor interactor = MockCoursesInteractor(); + MockCalendarEventsApi api = MockCalendarEventsApi(); + MockCalendarFilterDb filterDb = MockCalendarFilterDb(); + MockCoursesInteractor interactor = MockCoursesInteractor(); final String userDomain = 'user_domain'; final String userId = 'user_123'; @@ -130,8 +131,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfMonth(), - date.withEndOfMonth(), + date.withStartOfMonth()!, + date.withEndOfMonth()!, ScheduleItem.apiTypeAssignment, contexts: contexts, forceRefresh: false, @@ -141,8 +142,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfMonth(), - date.withEndOfMonth(), + date.withStartOfMonth()!, + date.withEndOfMonth()!, ScheduleItem.apiTypeCalendar, contexts: contexts, forceRefresh: false, @@ -195,8 +196,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfDay(), - date.withEndOfDay(), + date.withStartOfDay()!, + date.withEndOfDay()!, ScheduleItem.apiTypeAssignment, contexts: contexts, forceRefresh: true, @@ -206,8 +207,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfDay(), - date.withEndOfDay(), + date.withStartOfDay()!, + date.withEndOfDay()!, ScheduleItem.apiTypeCalendar, contexts: contexts, forceRefresh: true, @@ -238,8 +239,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfDay(), - date.withEndOfDay(), + date.withStartOfDay()!, + date.withEndOfDay()!, ScheduleItem.apiTypeAssignment, contexts: contexts, forceRefresh: true, @@ -249,8 +250,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfDay(), - date.withEndOfDay(), + date.withStartOfDay()!, + date.withEndOfDay()!, ScheduleItem.apiTypeCalendar, contexts: contexts, forceRefresh: true, @@ -281,8 +282,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfMonth(), - date.withEndOfMonth(), + date.withStartOfMonth()!, + date.withEndOfMonth()!, ScheduleItem.apiTypeCalendar, contexts: contexts, forceRefresh: true, @@ -292,8 +293,8 @@ void main() { verify( api.getUserCalendarItems( observeeId, - date.withStartOfMonth(), - date.withEndOfMonth(), + date.withStartOfMonth()!, + date.withEndOfMonth()!, ScheduleItem.apiTypeAssignment, contexts: contexts, forceRefresh: true, @@ -391,7 +392,7 @@ void main() { }); test('getContexts fetches courses and sets courseNameMap', () async { - when(filterDb.getByObserveeId(any, any, any)).thenAnswer((_) => null); + when(filterDb.getByObserveeId(any, any, any)).thenAnswer((_) => Future.value(null)); final fetcher = PlannerFetcher(userId: userId, userDomain: userDomain, observeeId: observeeId); @@ -403,7 +404,7 @@ void main() { expect(newContexts, contexts); expect(fetcher.courseNameMap[observeeId], isNotNull); - expect(fetcher.courseNameMap[observeeId][course.id], course.name); + expect(fetcher.courseNameMap[observeeId]?[course.id], course.name); }); test('fetchPlannerItems excludes hidden items', () async { @@ -414,7 +415,7 @@ void main() { var fetcher = PlannerFetcher(userId: "", userDomain: "", observeeId: observeeId); fetcher.courseNameMap[observeeId] = {}; - fetcher.courseNameMap[observeeId][courseId] = courseName; + fetcher.courseNameMap[observeeId]?[courseId] = courseName; var item = ScheduleItem((b) => b ..title = "Item" diff --git a/apps/flutter_parent/test/screens/courses/course_details_interactor_test.dart b/apps/flutter_parent/test/screens/courses/course_details_interactor_test.dart index 97fbbe5d31..aa3f287b29 100644 --- a/apps/flutter_parent/test/screens/courses/course_details_interactor_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_details_interactor_test.dart @@ -23,17 +23,18 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final courseId = '123'; final studentId = '1337'; final gradingPeriodId = '321'; - final _MockCourseApi courseApi = _MockCourseApi(); - final _MockAssignmentApi assignmentApi = _MockAssignmentApi(); + final MockCourseApi courseApi = MockCourseApi(); + final MockAssignmentApi assignmentApi = MockAssignmentApi(); final enrollmentApi = MockEnrollmentsApi(); - final _MockCalendarApi calendarApi = _MockCalendarApi(); - final _MockPageApi pageApi = _MockPageApi(); + final MockCalendarEventsApi calendarApi = MockCalendarEventsApi(); + final MockPageApi pageApi = MockPageApi(); setupTestLocator((locator) { locator.registerLazySingleton(() => courseApi); @@ -103,11 +104,3 @@ void main() { verify(courseApi.getCourseSettings(courseId, forceRefresh: true)).called(1); }); } - -class _MockCalendarApi extends Mock implements CalendarEventsApi {} - -class _MockCourseApi extends Mock implements CourseApi {} - -class _MockAssignmentApi extends Mock implements AssignmentApi {} - -class _MockPageApi extends Mock implements PageApi {} diff --git a/apps/flutter_parent/test/screens/courses/course_details_model_test.dart b/apps/flutter_parent/test/screens/courses/course_details_model_test.dart index 3285c6bf1f..745f2daf8f 100644 --- a/apps/flutter_parent/test/screens/courses/course_details_model_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_details_model_test.dart @@ -32,6 +32,7 @@ import 'package:tuple/tuple.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; const _studentId = '123'; const _studentName = 'billy jean'; @@ -182,7 +183,7 @@ void main() { test('Does not fail with an empty group response', () async { // Mock the data with null response when(interactor.loadAssignmentGroups(_courseId, _studentId, null, forceRefresh: false)) - .thenAnswer((_) async => null); + .thenAnswer((_) async => Future.value(null)); // Make the call to test final model = CourseDetailsModel.withCourse(_student, _course); @@ -239,7 +240,7 @@ void main() { expect(gradeDetails.assignmentGroups, [ publishedGroup, - unpublishedGroup.rebuild((b) => b..assignments = BuiltList.of(List()).toBuilder()) + unpublishedGroup.rebuild((b) => b..assignments = BuiltList.of([]).toBuilder()) ]); }); diff --git a/apps/flutter_parent/test/screens/courses/course_details_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_details_screen_test.dart index 573f4d8ecf..99c15a65b8 100644 --- a/apps/flutter_parent/test/screens/courses/course_details_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_details_screen_test.dart @@ -39,6 +39,7 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final studentId = '123'; @@ -48,8 +49,8 @@ void main() { ..id = studentId ..name = studentName); - final courseInteractor = _MockCourseDetailsInteractor(); - final convoInteractor = _MockCreateConversationInteractor(); + final courseInteractor = MockCourseDetailsInteractor(); + final convoInteractor = MockCreateConversationInteractor(); setupTestLocator((_locator) { _locator.registerFactory(() => courseInteractor); @@ -452,7 +453,7 @@ void main() { when(courseInteractor.loadFrontPage(courseId)).thenAnswer((_) async => CanvasPage((b) => b ..id = '1' ..body = 'hodor')); - + await tester.pumpWidget(TestApp(CourseDetailsScreen.withCourse(course))); await tester.pump(); // Widget creation await tester.pump(); // Future resolved @@ -462,8 +463,4 @@ void main() { verify(courseInteractor.loadCourse(courseId, forceRefresh: true)).called(1); // Refresh load }); -} - -class _MockCourseDetailsInteractor extends Mock implements CourseDetailsInteractor {} - -class _MockCreateConversationInteractor extends Mock implements CreateConversationInteractor {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/courses/course_front_page_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_front_page_screen_test.dart index f96a4e539c..35b9cb3cf8 100644 --- a/apps/flutter_parent/test/screens/courses/course_front_page_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_front_page_screen_test.dart @@ -25,6 +25,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final l10n = AppLocalizations(); @@ -33,7 +34,7 @@ void main() { final _page = CanvasPage((b) => b ..id = '1' ..body = ''); - final _interactor = _MockCourseDetailsInteractor(); + final _interactor = MockCourseDetailsInteractor(); setupTestLocator((locator) { locator.registerFactory(() => _interactor); @@ -79,6 +80,4 @@ void main() { expect(find.byType(WebView), findsOneWidget); }); -} - -class _MockCourseDetailsInteractor extends Mock implements CourseDetailsInteractor {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart index bf2c9aff64..7ef1f02a8a 100644 --- a/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_grades_screen_test.dart @@ -49,6 +49,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; const _studentId = '123'; const _courseId = '321'; @@ -135,7 +136,7 @@ void main() { testWidgetsWithAccessibilityChecks('Shows empty', (tester) async { final model = CourseDetailsModel(_student, _courseId); - when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => List()); + when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => []); await tester.pumpWidget(_testableWidget(model)); await tester.pump(); // Build the widget @@ -160,7 +161,7 @@ void main() { model.course = _mockCourse().rebuild((b) => b..hasGradingPeriods = true); // Mock stuff - when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => List()); + when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => []); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([gradingPeriod]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); @@ -181,7 +182,7 @@ void main() { testWidgetsWithAccessibilityChecks('Shows empty without period header', (tester) async { final model = CourseDetailsModel(_student, _courseId); - final gradingPeriod = null; + GradingPeriod? gradingPeriod = null; final enrollment = Enrollment((b) => b ..enrollmentState = 'active' ..grades = _mockGrade(currentScore: 12) @@ -190,7 +191,7 @@ void main() { model.course = _mockCourse().rebuild((b) => b..hasGradingPeriods = true); // Mock stuff - when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => List()); + when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => []); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); @@ -220,7 +221,7 @@ void main() { final model = CourseDetailsModel(_student, _courseId); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => [group]); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( - (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); model.course = _mockCourse(); @@ -231,11 +232,11 @@ void main() { expect(find.text(AppLocalizations().noGrade), findsOneWidget); expect(find.text(group.name), findsOneWidget); - expect(find.text(group.assignments.first.name), findsOneWidget); + expect(find.text(group.assignments.first.name!), findsOneWidget); expect(find.text('Due Jan 1 at 12:00 AM'), findsOneWidget); expect(find.text('- / 0'), findsOneWidget); - expect(find.text(group.assignments.last.name), findsOneWidget); + expect(find.text(group.assignments.last.name!), findsOneWidget); expect(find.text('No Due Date'), findsOneWidget); expect(find.text('$grade / 2.2'), findsOneWidget); }); @@ -253,7 +254,7 @@ void main() { model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( - (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); await tester.pumpWidget(_testableWidget(model)); @@ -263,7 +264,7 @@ void main() { expect(find.text(groups.first.name), findsNothing); expect(find.text(groups.last.name), findsOneWidget); - expect(find.text(groups.last.assignments.first.name), findsOneWidget); + expect(find.text(groups.last.assignments.first.name!), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Does not show assignments when group is collapsed', (tester) async { @@ -276,7 +277,7 @@ void main() { model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( - (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); await tester.pumpWidget(_testableWidget(model)); @@ -285,12 +286,12 @@ void main() { final groupHeader = find.text(groups.first.name); expect(groupHeader, findsOneWidget); - expect(find.text(groups.first.assignments.first.name), findsOneWidget); + expect(find.text(groups.first.assignments.first.name!), findsOneWidget); await tester.tap(groupHeader); await tester.pumpAndSettle(); - expect(find.text(groups.first.assignments.first.name), findsNothing); + expect(find.text(groups.first.assignments.first.name!), findsNothing); }); testWidgetsWithAccessibilityChecks('Shows assignment statuses', (tester) async { @@ -307,7 +308,7 @@ void main() { final model = CourseDetailsModel(_student, _courseId); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => [group]); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( - (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); model.course = _mockCourse(); @@ -329,7 +330,7 @@ void main() { final model = CourseDetailsModel(_student, _courseId); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => [group]); when(interactor.loadGradingPeriods(_courseId)) - .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); model.course = _mockCourse(); @@ -348,7 +349,7 @@ void main() { final model = CourseDetailsModel(_student, _courseId); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => [group]); when(interactor.loadGradingPeriods(_courseId)) - .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); model.course = _mockCourse().rebuild((b) => b..settings.restrictQuantitativeData = true); @@ -368,7 +369,7 @@ void main() { final model = CourseDetailsModel(_student, _courseId); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => [group]); when(interactor.loadGradingPeriods(_courseId)) - .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); model.course = _mockCourse().rebuild((b) => b..settings.restrictQuantitativeData = true); @@ -392,7 +393,7 @@ void main() { model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer((_) async => - GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)) .thenAnswer((_) async => [enrollment]); @@ -415,7 +416,7 @@ void main() { model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer((_) async => - GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)) .thenAnswer((_) async => [enrollment]); @@ -437,7 +438,7 @@ void main() { model.course = _mockCourse().rebuild((b) => b..hasGradingPeriods = true); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer((_) async => - GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)) .thenAnswer((_) async => [enrollment]); @@ -461,7 +462,7 @@ void main() { model.courseSettings = CourseSettings((b) => b..restrictQuantitativeData = true); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)) - .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); await tester.pumpWidget(_testableWidget(model)); @@ -485,7 +486,7 @@ void main() { model.courseSettings = CourseSettings((b) => b..restrictQuantitativeData = true); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)) - .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + .thenAnswer((_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); await tester.pumpWidget(_testableWidget(model)); @@ -554,7 +555,7 @@ void main() { when(interactor.loadAssignmentGroups(_courseId, _studentId, gradingPeriod.id)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([gradingPeriod]).toBuilder())); - when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => null); + when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => Future.value(null)); await tester.pumpWidget(_testableWidget(model)); await tester.pump(); // Build the widget @@ -593,7 +594,7 @@ void main() { when(interactor.loadAssignmentGroups(_courseId, _studentId, gradingPeriod.id)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([gradingPeriod]).toBuilder())); - when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => null); + when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => Future.value(null)); await tester.pumpWidget(_testableWidget(model)); await tester.pump(); // Build the widget @@ -634,7 +635,7 @@ void main() { when(interactor.loadAssignmentGroups(_courseId, _studentId, gradingPeriod.id)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([gradingPeriod]).toBuilder())); - when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => null); + when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => Future.value(null)); await tester.pumpWidget(_testableWidget(model)); await tester.pump(); // Build the widget @@ -654,7 +655,7 @@ void main() { model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( - (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); await tester.pumpWidget(_testableWidget(model)); @@ -670,7 +671,7 @@ void main() { testWidgetsWithAccessibilityChecks( 'grading period is shown for multiple grading periods when all grading periods is selected and no assignments exist', (tester) async { - final groups = List(); + final groups = []; final gradingPeriod = GradingPeriod((b) => b ..id = '123' ..title = 'Other'); @@ -742,7 +743,7 @@ void main() { model.course = _mockCourse(); when(interactor.loadAssignmentGroups(_courseId, _studentId, null)).thenAnswer((_) async => groups); when(interactor.loadGradingPeriods(_courseId)).thenAnswer( - (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of(List()).toBuilder())); + (_) async => GradingPeriodResponse((b) => b..gradingPeriods = BuiltList.of([]).toBuilder())); when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); await tester.pumpWidget(_testableWidget(model, @@ -751,7 +752,7 @@ void main() { await tester.pump(); // Build the widget await tester.pump(); // Let the future finish - await tester.tap(find.text(groups.first.assignments.first.name)); + await tester.tap(find.text(groups.first.assignments.first.name!)); await tester.pump(); // Process the tap await tester.pump(); // Show the widget @@ -776,7 +777,7 @@ Course _mockCourse() { ..gradingScheme = _gradingSchemeBuilder); } -GradeBuilder _mockGrade({double currentScore, double finalScore, String currentGrade, String finalGrade}) { +GradeBuilder _mockGrade({double? currentScore, double? finalScore, String? currentGrade, String? finalGrade}) { return GradeBuilder() ..htmlUrl = '' ..currentScore = currentScore ?? 0 @@ -800,10 +801,10 @@ AssignmentGroup _mockAssignmentGroup({ Assignment _mockAssignment({ String id = '0', String groupId = _assignmentGroupId, - Submission submission, - DateTime dueAt, + Submission? submission, + DateTime? dueAt, double pointsPossible = 0, - GradingType gradingType + GradingType? gradingType }) { return Assignment((b) => b ..id = id @@ -819,7 +820,7 @@ Assignment _mockAssignment({ ..gradingType = gradingType); } -Submission _mockSubmission({String assignmentId = '', String grade, bool isLate, DateTime submittedAt, double score}) { +Submission _mockSubmission({String assignmentId = '', String? grade, bool? isLate, DateTime? submittedAt, double? score}) { return Submission((b) => b ..userId = _studentId ..assignmentId = assignmentId diff --git a/apps/flutter_parent/test/screens/courses/course_routing_shell_interactor_test.dart b/apps/flutter_parent/test/screens/courses/course_routing_shell_interactor_test.dart index 38f36f3703..9be603f34f 100644 --- a/apps/flutter_parent/test/screens/courses/course_routing_shell_interactor_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_routing_shell_interactor_test.dart @@ -25,6 +25,7 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final mockCourseApi = MockCourseApi(); @@ -58,8 +59,8 @@ void main() { final result = await interactor.loadCourseShell(CourseShellType.syllabus, course.id); - expect(result.frontPage, isNull); - expect(result.course, isNotNull); + expect(result?.frontPage, isNull); + expect(result?.course, isNotNull); }); test('returns error when course syllabus is null for syllabus type', () async { @@ -84,8 +85,8 @@ void main() { final result = await interactor.loadCourseShell(CourseShellType.frontPage, course.id); - expect(result.frontPage, isNotNull); - expect(result.course, isNotNull); + expect(result?.frontPage, isNotNull); + expect(result?.course, isNotNull); }); test('returns error when course front page hass null body for frontPage type', () async { diff --git a/apps/flutter_parent/test/screens/courses/course_routing_shell_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_routing_shell_screen_test.dart index 9a29f84287..d578d325d8 100644 --- a/apps/flutter_parent/test/screens/courses/course_routing_shell_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_routing_shell_screen_test.dart @@ -32,6 +32,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final interactor = MockCourseRoutingShellInteractor(); @@ -102,14 +103,14 @@ void main() { expect(find.text(AppLocalizations().unexpectedError), findsOneWidget); // Should have the error widgets button for refresh - final matchedWidget = find.byType(FlatButton); + final matchedWidget = find.byType(TextButton); expect(matchedWidget, findsOneWidget); // Try to refresh await tester.tap(matchedWidget); await tester.pumpAndSettle(); - verify(interactor.loadCourseShell(any, any)).called(2); // Once for initial load, another for the refresh + verify(interactor.loadCourseShell(any, any, forceRefresh: anyNamed('forceRefresh'))).called(2); // Once for initial load, another for the refresh }); testWidgetsWithAccessibilityChecks('Refresh displays loading indicator and loads state', (tester) async { diff --git a/apps/flutter_parent/test/screens/courses/course_summary_screen_test.dart b/apps/flutter_parent/test/screens/courses/course_summary_screen_test.dart index bfff85b7a5..85f87208f9 100644 --- a/apps/flutter_parent/test/screens/courses/course_summary_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/course_summary_screen_test.dart @@ -46,6 +46,7 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; final studentId = '1234'; final studentName = 'billy jean'; @@ -66,7 +67,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Loads data using model', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); when(model.loadSummary(refresh: false)).thenAnswer((_) async => []); await tester.pumpWidget(_testableWidget(model)); @@ -76,7 +77,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays loading indicator', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); Completer> completer = Completer(); when(model.loadSummary(refresh: false)).thenAnswer((_) => completer.future); @@ -88,7 +89,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays empty state', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); when(model.loadSummary(refresh: false)).thenAnswer((_) async => []); await tester.pumpWidget(_testableWidget(model)); @@ -100,7 +101,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays error state', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); when(model.loadSummary(refresh: false)).thenAnswer((_) => Future.error('')); @@ -111,7 +112,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Refreshes from error state', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); when(model.loadSummary(refresh: false)).thenAnswer((_) => Future.error('')); @@ -128,7 +129,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Refreshes from empty state', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); when(model.loadSummary(refresh: false)).thenAnswer((_) async => []); @@ -146,7 +147,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays details for calendar event', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); final event = ScheduleItem((s) => s ..type = ScheduleItem.apiTypeCalendar @@ -157,13 +158,13 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - expect(find.text(event.title), findsOneWidget); - expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)), findsOneWidget); + expect(find.text(event.title!), findsOneWidget); + expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.calendar_month), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays details for assignment', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); final event = ScheduleItem((s) => s ..type = ScheduleItem.apiTypeAssignment @@ -180,13 +181,13 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - expect(find.text(event.title), findsOneWidget); - expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)), findsOneWidget); + expect(find.text(event.title!), findsOneWidget); + expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.assignment), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays details for discussion assignment', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); final event = ScheduleItem((s) => s ..type = ScheduleItem.apiTypeAssignment @@ -203,13 +204,13 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - expect(find.text(event.title), findsOneWidget); - expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)), findsOneWidget); + expect(find.text(event.title!), findsOneWidget); + expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.discussion), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays details for quiz assignment', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); final event = ScheduleItem((s) => s ..type = ScheduleItem.apiTypeAssignment @@ -226,13 +227,13 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - expect(find.text(event.title), findsOneWidget); - expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)), findsOneWidget); + expect(find.text(event.title!), findsOneWidget); + expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.quiz), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays details for locked assignment', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); final event = ScheduleItem((s) => s ..type = ScheduleItem.apiTypeAssignment @@ -250,13 +251,13 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - expect(find.text(event.title), findsOneWidget); - expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)), findsOneWidget); + expect(find.text(event.title!), findsOneWidget); + expect(find.text(event.startAt.l10nFormat(l10n.dateAtTime)!), findsOneWidget); expect(find.byIcon(CanvasIcons.lock), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays details for undated assignment', (tester) async { - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); final event = ScheduleItem((s) => s ..type = ScheduleItem.apiTypeAssignment @@ -267,13 +268,16 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - expect(find.text(event.title), findsOneWidget); + expect(find.text(event.title!), findsOneWidget); expect(find.text(l10n.noDueDate), findsOneWidget); expect(find.byIcon(CanvasIcons.assignment), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Tapping assignment item loads assignment details', (tester) async { - final event = ScheduleItem((s) => s + await tester.runAsync(() async { + final event = ScheduleItem((s) + => + s ..type = ScheduleItem.apiTypeAssignment ..title = 'Normal Assignment' ..startAt = DateTime.now() @@ -282,34 +286,42 @@ void main() { ..courseId = '' ..assignmentGroupId = '' ..position = 0 - ..submissionTypes = ListBuilder([]))); + ..submissionTypes = ListBuilder([])) + ); - final model = MockCourseModel(); - when(model.courseId).thenReturn('course_123'); - when(model.student).thenReturn(student); - when(model.course).thenReturn(Course((c) => c..courseCode = 'CRS 123')); - when(model.loadSummary(refresh: false)).thenAnswer((_) async => [event]); + final model = MockCourseDetailsModel(); + when(model.courseId).thenReturn('course_123'); + when(model.student).thenReturn(student); + when(model.course).thenReturn(Course((c) => c..courseCode = 'CRS 123')); + when(model.loadSummary(refresh: false)).thenAnswer((_) async => [event]); - var interactor = MockAssignmentDetailsInteractor(); - setupTestLocator((locator) { + var interactor = MockAssignmentDetailsInteractor(); + setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => QuickNav()); - }); - var observer = MockNavigatorObserver(); - await tester.pumpWidget(_testableWidget( + }); + when(interactor.loadAssignmentDetails(any, 'course_123', 'assignment_123', studentId)) + .thenAnswer((_) async => Future.value(AssignmentDetails(course: model.course, assignment: event.assignment))); + + var observer = MockNavigatorObserver(); + await tester.pumpWidget(_testableWidget( model, observers: [observer], platformConfig: PlatformConfig( - mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}, initLoggedInUser: login), - )); - await tester.pumpAndSettle(); + mockApiPrefs: {ApiPrefs.KEY_CURRENT_STUDENT: json.encode(serialize(student))}, initLoggedInUser: login), + )); + await tester.pump(Duration(seconds: 2)); + await tester.pumpAndSettle(); - await tester.tap(find.text(event.title)); + await tester.tap(find.text(event.title!)); - await tester.pump(); - await tester.pump(); + await tester.pump(Duration(seconds: 2)); + await tester.pump(); + await tester.pump(); + await tester.pumpAndSettle(); - expect(find.byType(AssignmentDetailsScreen), findsOneWidget); + expect(find.byType(AssignmentDetailsScreen), findsOneWidget); + }); }); testWidgetsWithAccessibilityChecks('Tapping calendar event item loads event details', (tester) async { @@ -318,7 +330,7 @@ void main() { ..title = 'Normal Event' ..startAt = DateTime.now()); - final model = MockCourseModel(); + final model = MockCourseDetailsModel(); when(model.loadSummary(refresh: false)).thenAnswer((_) async => [event]); when(model.student).thenAnswer((_) => student); @@ -331,7 +343,7 @@ void main() { await tester.pumpWidget(_testableWidget(model)); await tester.pumpAndSettle(); - await tester.tap(find.text(event.title)); + await tester.tap(find.text(event.title!)); await tester.pump(); await tester.pump(); diff --git a/apps/flutter_parent/test/screens/courses/courses_interactor_test.dart b/apps/flutter_parent/test/screens/courses/courses_interactor_test.dart index 423c28cf56..819135e45a 100644 --- a/apps/flutter_parent/test/screens/courses/courses_interactor_test.dart +++ b/apps/flutter_parent/test/screens/courses/courses_interactor_test.dart @@ -28,6 +28,7 @@ import 'package:test/test.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final _api = MockCourseApi(); @@ -90,8 +91,8 @@ void main() { final result = await CoursesInteractor().getCourses(isRefresh: true, studentId: null); verify(_api.getObserveeCourses(forceRefresh: true)); - expect(result.length, 1); - expect(result.first.id, _course.id); + expect(result?.length, 1); + expect(result?.first.id, _course.id); }); test('getCourses returns only courses for studentId parameter and is valid', () async { @@ -100,8 +101,8 @@ void main() { final result = await CoursesInteractor().getCourses(isRefresh: true, studentId: _invalidStudentId); verify(_api.getObserveeCourses(forceRefresh: true)); - expect(result.length, 1); - expect(result.first.id, _invalidCourse.id); + expect(result?.length, 1); + expect(result?.first.id, _invalidCourse.id); }); test('getCourses returns no courses for invalid studentId but valid dates', () async { diff --git a/apps/flutter_parent/test/screens/courses/courses_screen_test.dart b/apps/flutter_parent/test/screens/courses/courses_screen_test.dart index 2160f90269..864d68f7de 100644 --- a/apps/flutter_parent/test/screens/courses/courses_screen_test.dart +++ b/apps/flutter_parent/test/screens/courses/courses_screen_test.dart @@ -42,11 +42,12 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { AppLocalizations l10n = AppLocalizations(); - _setupLocator(CoursesInteractor mockInteractor, {SelectedStudentNotifier notifier}) { + _setupLocator(CoursesInteractor mockInteractor, {SelectedStudentNotifier? notifier}) { setupTestLocator((locator) { locator.registerFactory(() => mockInteractor); locator.registerFactory(() => _MockCourseDetailsInteractor()); @@ -60,7 +61,7 @@ void main() { }); } - Widget _testableMaterialWidget({Widget widget, SelectedStudentNotifier notifier = null}) => TestApp( + Widget _testableMaterialWidget({Widget? widget, SelectedStudentNotifier? notifier = null}) => TestApp( ChangeNotifierProvider( create: (context) => notifier ?? SelectedStudentNotifier() ..value = _mockStudent('1'), @@ -305,20 +306,20 @@ void main() { expect(find.byType(CourseDetailsScreen), findsOneWidget); - ApiPrefs.clean(); + await ApiPrefs.clean(); }); }); } class _MockedCoursesInteractor extends CoursesInteractor { - List courses; + List? courses; bool error = false; _MockedCoursesInteractor({this.courses}); @override - Future> getCourses({bool isRefresh = false, String studentId = null}) async { + Future> getCourses({bool isRefresh = false, String? studentId = null}) async { if (error) throw ''; return courses ?? [_mockCourse('1')]; } @@ -342,8 +343,8 @@ List generateCoursesForStudent(String userId, {int numberOfCourses = 1}) Enrollment _mockEnrollment( String courseId, { String userId = '0', - String computedCurrentGrade, - double computedCurrentScore, + String? computedCurrentGrade, + double? computedCurrentScore, }) => Enrollment((b) => b ..courseId = courseId @@ -355,7 +356,7 @@ Enrollment _mockEnrollment( ..build()); Course _mockCourse(String courseId, - {ListBuilder enrollments, bool hasActiveGradingPeriod, double currentScore, String currentGrade}) => + {ListBuilder? enrollments, bool? hasActiveGradingPeriod, double? currentScore, String? currentGrade}) => Course((b) => b ..id = courseId ..name = 'CourseName' diff --git a/apps/flutter_parent/test/screens/crash_screen_test.dart b/apps/flutter_parent/test/screens/crash_screen_test.dart index 43c70ebad1..04530d058c 100644 --- a/apps/flutter_parent/test/screens/crash_screen_test.dart +++ b/apps/flutter_parent/test/screens/crash_screen_test.dart @@ -25,10 +25,11 @@ import 'package:flutter_test/flutter_test.dart'; import '../utils/accessibility_utils.dart'; import '../utils/test_app.dart'; import '../utils/test_helpers/mock_helpers.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { setupTestLocator((locator) { - locator.registerLazySingleton(() => MockFirebase()); + locator.registerLazySingleton(() => MockFirebaseCrashlytics()); }); setUp(() { @@ -102,13 +103,13 @@ void main() { expect(find.text(l10n.crashScreenMessage), findsOneWidget); // 'Contact support' button - expect(find.widgetWithText(FlatButton, l10n.crashScreenContact), findsOneWidget); + expect(find.widgetWithText(TextButton, l10n.crashScreenContact), findsOneWidget); // 'View error details' button - expect(find.widgetWithText(FlatButton, l10n.crashScreenViewDetails), findsOneWidget); + expect(find.widgetWithText(TextButton, l10n.crashScreenViewDetails), findsOneWidget); // 'Restart app' button - expect(find.widgetWithText(FlatButton, l10n.crashScreenRestart), findsOneWidget); + expect(find.widgetWithText(TextButton, l10n.crashScreenRestart), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays report error dialog', (tester) async { @@ -116,8 +117,8 @@ void main() { await tester.pumpAndSettle(); // 'Contact support' button - expect(find.widgetWithText(FlatButton, l10n.crashScreenContact), findsOneWidget); - await tester.tap(find.widgetWithText(FlatButton, l10n.crashScreenContact)); + expect(find.widgetWithText(TextButton, l10n.crashScreenContact), findsOneWidget); + await tester.tap(find.widgetWithText(TextButton, l10n.crashScreenContact)); await tester.pumpAndSettle(); expect(find.byType(ErrorReportDialog), findsOneWidget); @@ -198,12 +199,12 @@ class __CrashTestWidgetState extends State<_CrashTestWidget> { 'Count: $_counter', key: _CrashTestWidget.counterKey, ), - FlatButton( + TextButton( key: _CrashTestWidget.incrementKey, child: Text('Tap to increment'), onPressed: () => setState(() => _counter++), ), - FlatButton( + TextButton( key: _CrashTestWidget.crashKey, child: Text('Tap to crash'), onPressed: () { diff --git a/apps/flutter_parent/test/screens/dashboard/alert_notifier_test.dart b/apps/flutter_parent/test/screens/dashboard/alert_notifier_test.dart index 8a22ba3cc8..62834d1ad2 100644 --- a/apps/flutter_parent/test/screens/dashboard/alert_notifier_test.dart +++ b/apps/flutter_parent/test/screens/dashboard/alert_notifier_test.dart @@ -22,6 +22,7 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final api = MockAlertsApi(); diff --git a/apps/flutter_parent/test/screens/dashboard/dashboard_interactor_test.dart b/apps/flutter_parent/test/screens/dashboard/dashboard_interactor_test.dart index 1c50001d36..0d4b961172 100644 --- a/apps/flutter_parent/test/screens/dashboard/dashboard_interactor_test.dart +++ b/apps/flutter_parent/test/screens/dashboard/dashboard_interactor_test.dart @@ -29,6 +29,7 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { test('getStudents calls getObserveeEnrollments from EnrollmentsApi', () async { @@ -139,7 +140,7 @@ void main() { }); test('shouldShowOldReminderMessage calls OldAppMigration.hasOldReminders', () async { - var migration = _MockMigration(); + var migration = MockOldAppMigration(); await setupTestLocator((locator) { locator.registerLazySingleton(() => migration); }); @@ -154,11 +155,7 @@ User _mockStudent(String name) => User((b) => b ..sortableName = name ..build()); -Enrollment _mockEnrollment(UserBuilder observedUser) => Enrollment((b) => b +Enrollment _mockEnrollment(UserBuilder? observedUser) => Enrollment((b) => b ..enrollmentState = '' ..observedUser = observedUser ..build()); - -class _MockMigration extends Mock implements OldAppMigration {} - -class MockUserApi extends Mock implements UserApi {} diff --git a/apps/flutter_parent/test/screens/dashboard/dashboard_screen_test.dart b/apps/flutter_parent/test/screens/dashboard/dashboard_screen_test.dart index a6ecd53f35..14439b82e8 100644 --- a/apps/flutter_parent/test/screens/dashboard/dashboard_screen_test.dart +++ b/apps/flutter_parent/test/screens/dashboard/dashboard_screen_test.dart @@ -12,6 +12,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:math'; + import 'package:built_value/json_object.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; @@ -62,6 +64,7 @@ import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:mockito/mockito.dart'; +import 'package:permission_handler/permission_handler.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/canvas_model_utils.dart'; @@ -69,6 +72,7 @@ import '../../utils/network_image_response.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import '../../utils/test_utils.dart'; import '../courses/course_summary_screen_test.dart'; @@ -77,10 +81,10 @@ import '../courses/course_summary_screen_test.dart'; */ void main() { mockNetworkImageResponse(); - final analyticsMock = _MockAnalytics(); + final analyticsMock = MockAnalytics(); final alertsHelper = AlertsHelper(); - _setupLocator({MockInteractor interactor, AlertsApi alertsApi, InboxApi inboxApi}) async { + _setupLocator({MockInteractor? interactor, AlertsApi? alertsApi, InboxApi? inboxApi}) async { await setupTestLocator((locator) { locator.registerFactory(() => MockAlertsInteractor()); locator.registerFactory(() => MockCoursesInteractor()); @@ -89,7 +93,7 @@ void main() { locator.registerFactory(() => MockManageStudentsInteractor()); locator.registerFactory(() => MasqueradeScreenInteractor()); locator.registerFactory(() => SettingsInteractor()); - locator.registerLazySingleton(() => alertsApi ?? AlertsApiMock()); + locator.registerLazySingleton(() => alertsApi ?? MockAlertsApi()); locator.registerLazySingleton(() => AlertCountNotifier()); locator.registerLazySingleton(() => analyticsMock); locator.registerLazySingleton(() => CalendarTodayClickNotifier()); @@ -115,9 +119,9 @@ void main() { }); Widget _testableMaterialWidget({ - Login initLogin, - Map deepLinkParams, - DashboardContentScreens startingPage, + Login? initLogin, + Map? deepLinkParams, + DashboardContentScreens? startingPage, }) => TestApp( Scaffold( @@ -135,15 +139,15 @@ void main() { // Get the first user var interactor = GetIt.instance.get(); - User first; + late User? first; interactor.getStudents().then((students) { - first = students.first; + first = students?.first; }); await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); - expect(find.text('${first.shortName} (${first.pronouns})'), findsOneWidget); + expect(find.text('${first?.shortName} (${first?.pronouns})'), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Displays name without pronouns when pronouns are null', (tester) async { @@ -151,16 +155,16 @@ void main() { // Get the first user var interactor = GetIt.instance.get(); - User first; + late User? first; interactor.getStudents().then((students) { - first = students.first; + first = students?.first; }); await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); // Will find two, one in the navbar header and one in the student switcher - expect(find.text('${first.shortName}'), findsNWidgets(2)); + expect(find.text('${first?.shortName}'), findsNWidgets(2)); }); testWidgetsWithAccessibilityChecks( @@ -194,7 +198,7 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text(l10n.actAsUser), findsNothing); @@ -214,7 +218,7 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text(l10n.actAsUser), findsOneWidget); @@ -236,7 +240,7 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text(l10n.actAsUser), findsNothing); @@ -249,16 +253,16 @@ void main() { // Get the first user var interactor = GetIt.instance.get(); - User observer; + late User observer; interactor.getSelf().then((self) { - observer = self; + observer = self ?? User(); }); await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text('${observer.name} (${observer.pronouns})'), findsOneWidget); @@ -271,16 +275,16 @@ void main() { // Get the first user var interactor = GetIt.instance.get(); - User observer; + late User observer; interactor.getSelf().then((self) { - observer = self; + observer = self ?? User(); }); await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text('${observer.name}'), findsOneWidget); @@ -311,7 +315,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Assert there's no text in the inbox-count @@ -329,7 +333,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text('12321'), findsOneWidget); @@ -465,7 +469,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Inbox @@ -480,7 +484,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Manage Students @@ -498,7 +502,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Settings @@ -525,7 +529,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Help @@ -543,7 +547,7 @@ void main() { 'tapping Sign Out from nav drawer displays confirmation dialog', (tester) async { final reminderDb = MockReminderDb(); - final notificationUtil = _MockNotificationUtil(); + final notificationUtil = MockNotificationUtil(); await _setupLocator(); final _locator = GetIt.instance; @@ -563,7 +567,7 @@ void main() { expect(ApiPrefs.isLoggedIn(), true); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Sign Out @@ -594,9 +598,9 @@ void main() { testWidgets('tapping Sign Out from nav drawer signs user out and returns to the Login Landing screen', (tester) async { final reminderDb = MockReminderDb(); - final calendarFilterDb = _MockCalendarFilterDb(); - final notificationUtil = _MockNotificationUtil(); - final authApi = _MockAuthApi(); + final calendarFilterDb = MockCalendarFilterDb(); + final notificationUtil = MockNotificationUtil(); + final authApi = MockAuthApi(); await _setupLocator(); final _locator = GetIt.instance; @@ -618,7 +622,7 @@ void main() { expect(ApiPrefs.isLoggedIn(), true); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Sign Out @@ -644,8 +648,8 @@ void main() { testWidgets('tapping Switch Users from nav drawer signs user out and returns to the Login Landing screen', (tester) async { final reminderDb = MockReminderDb(); - final calendarFilterDb = _MockCalendarFilterDb(); - final notificationUtil = _MockNotificationUtil(); + final calendarFilterDb = MockCalendarFilterDb(); + final notificationUtil = MockNotificationUtil(); await _setupLocator(); final _locator = GetIt.instance; @@ -666,7 +670,7 @@ void main() { expect(ApiPrefs.isLoggedIn(), true); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Click on Sign Out @@ -691,9 +695,9 @@ void main() { // Get the first user var interactor = GetIt.instance.get(); - User first; + late User? first; interactor.getStudents().then((students) { - first = students.first; + first = students?.first; }); // Load the screen @@ -707,17 +711,18 @@ void main() { expect(find.byType(StudentExpansionWidget), findsOneWidget); expect(slideAnimation.value, retracted); + expect(first, isNotNull); // Tap the user header, expanding it // There will be two instances, one in the header and one in the student switcher // we want to tap the first one (the one in the header) - await tester.tap(find.text(first.shortName).at(0)); + await tester.tap(find.text(first!.shortName!).at(0)); await tester.pumpAndSettle(); // Wait for user switcher to slide out expect(slideAnimation.value, expanded); // Tap the user header, retracting it // There will be two instances, one in the header and one in the student switcher // we want to tap the first one (the one in the header) - await tester.tap(find.text(first.shortName).at(0)); + await tester.tap(find.text(first!.shortName!).at(0)); await tester.pumpAndSettle(); // Wait for user switcher to slide back expect(slideAnimation.value, retracted); }); @@ -731,11 +736,11 @@ void main() { // Get the first user var interactor = GetIt.instance.get(); - User first; - User second; + late User? first; + late User? second; interactor.getStudents().then((students) { - first = students.first; - second = students[1]; + first = students?.first; + second = students?[1]; }); // Load the screen @@ -749,15 +754,18 @@ void main() { expect(find.byType(StudentExpansionWidget), findsOneWidget); expect(slideAnimation.value, retracted); + expect(first, isNotNull); + expect(second, isNotNull); + // Tap the user header, expanding it // There will be two instances, one in the header and one in the student switcher // we want to tap the first one (the one in the header) - await tester.tap(find.text(first.shortName).at(0)); + await tester.tap(find.text(first!.shortName!).at(0)); await tester.pumpAndSettle(); // Wait for user switcher to slide out expect(slideAnimation.value, expanded); // Tap on a user - await tester.tap(find.text(second.shortName)); + await tester.tap(find.text(second!.shortName!)); await tester.pumpAndSettle(); // Wait for user switcher to slide back expect(slideAnimation.value, retracted); @@ -794,13 +802,14 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Tap the 'Act As User' button await tester.tap(find.text(l10n.actAsUser)); await tester.pump(); await tester.pump(); + await tester.pumpAndSettle(); expect(find.byType(MasqueradeScreen), findsOneWidget); }); @@ -820,13 +829,14 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Tap the 'Stop Acting As User' button await tester.tap(find.text(l10n.stopActAsUser)); await tester.pump(); await tester.pump(); + await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); expect(find.text(l10n.endMasqueradeMessage(login.user.name)), findsOneWidget); @@ -891,7 +901,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); verify(inboxApi.getUnreadCount()).called(1); @@ -908,7 +918,7 @@ void main() { await tester.pumpAndSettle(); // Open the nav drawer - dashboardState(tester).scaffoldKey.currentState.openDrawer(); + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); when(inboxApi.getUnreadCount()) @@ -930,6 +940,8 @@ void main() { await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); + // Open the nav drawer + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); verify(alertsApi.getAlertsDepaginated(any, any)).called(1); @@ -944,6 +956,8 @@ void main() { await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); + // Open the nav drawer + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Assert there's no text in the alerts-count @@ -971,6 +985,8 @@ void main() { await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); + // Open the nav drawer + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); expect(find.text('5'), findsOneWidget); @@ -997,6 +1013,8 @@ void main() { await tester.pumpWidget(_testableMaterialWidget()); await tester.pumpAndSettle(); + // Open the nav drawer + dashboardState(tester).scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); when(alertsApi.getAlertsDepaginated(any, any)).thenAnswer((_) => Future.value(data.sublist(0, 4).toList())); @@ -1219,10 +1237,6 @@ class MockHelpScreenInteractor extends HelpScreenInteractor { Future> getObserverCustomHelpLinks({bool forceRefresh = false}) => Future.value([]); } -class MockAlertsInteractor extends AlertsInteractor {} - -class AlertsApiMock extends Mock implements AlertsApi {} - class MockInteractor extends DashboardInteractor { bool includePronouns; bool generateStudents; @@ -1244,7 +1258,7 @@ class MockInteractor extends DashboardInteractor { : []; @override - Future getSelf({app}) async => generateSelf + Future getSelf({app}) async => generateSelf ? CanvasModelTestUtils.mockUser( name: 'Marlene Name', shortName: 'Marlene', @@ -1254,12 +1268,15 @@ class MockInteractor extends DashboardInteractor { @override Future shouldShowOldReminderMessage() async => showOldReminderMessage; + + @override + Future requestNotificationPermission() async => PermissionStatus.granted; } class MockCoursesInteractor extends CoursesInteractor { @override - Future> getCourses({bool isRefresh = false, String studentId = null}) async { - var courses = List(); + Future> getCourses({bool isRefresh = false, String? studentId = null}) async { + var courses = []; return courses; } } @@ -1268,13 +1285,3 @@ class MockManageStudentsInteractor extends ManageStudentsInteractor { @override Future> getStudents({bool forceRefresh = false}) => Future.value([]); } - -class _MockCalendarFilterDb extends Mock implements CalendarFilterDb {} - -class _MockNotificationUtil extends Mock implements NotificationUtil {} - -class MockPlannerApi extends Mock implements PlannerApi {} - -class _MockAnalytics extends Mock implements Analytics {} - -class _MockAuthApi extends Mock implements AuthApi {} diff --git a/apps/flutter_parent/test/screens/dashboard/student_horizontal_list_view_test.dart b/apps/flutter_parent/test/screens/dashboard/student_horizontal_list_view_test.dart index 7d4bb919a6..143f7ae923 100644 --- a/apps/flutter_parent/test/screens/dashboard/student_horizontal_list_view_test.dart +++ b/apps/flutter_parent/test/screens/dashboard/student_horizontal_list_view_test.dart @@ -31,6 +31,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/canvas_model_utils.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { group('Render', () { @@ -50,9 +51,9 @@ void main() { await tester.pumpAndSettle(); // Check for the names of the students and the add student button - expect(find.text(student1.shortName), findsOneWidget); - expect(find.text(student2.shortName), findsOneWidget); - expect(find.text(student3.shortName), findsOneWidget); + expect(find.text(student1.shortName!), findsOneWidget); + expect(find.text(student2.shortName!), findsOneWidget); + expect(find.text(student3.shortName!), findsOneWidget); expect(find.text(AppLocalizations().addStudent), findsOneWidget); }); }); @@ -87,14 +88,14 @@ void main() { expect(notifier.value, null); // Check the initial value of the theme, should be default student color (electric) - var state = ParentTheme.of(TestApp.navigatorKey.currentContext); - expect(state.studentColor, StudentColorSet.electric.light); + var state = ParentTheme.of(TestApp.navigatorKey.currentContext!); + expect(state?.studentColor, StudentColorSet.electric.light); // Check for the second student - expect(find.text(student2.shortName), findsOneWidget); + expect(find.text(student2.shortName!), findsOneWidget); // Tap on the student - await tester.tap(find.text(student2.shortName)); + await tester.tap(find.text(student2.shortName!)); await tester.pumpAndSettle(); // Wait for animations to settle await tester.pump(); @@ -102,7 +103,7 @@ void main() { expect(student2, notifier.value); // Check the theme, should be correct color for student - var expectedColor = (await state.getColorsForStudent(student2.id)).light; + var expectedColor = (await state!.getColorsForStudent(student2.id)).light; expect(state.studentColor, expectedColor); // Check to make sure we called the onTap function passed in @@ -113,8 +114,8 @@ void main() { testWidgetsWithAccessibilityChecks('add student tap calls PairingUtil', (tester) async { var student1 = CanvasModelTestUtils.mockUser(name: 'Billy'); - PairingUtil pairingUtil = MockPairingUtil(); - AccountsApi accountsApi = MockAccountsApi(); + MockPairingUtil pairingUtil = MockPairingUtil(); + MockAccountsApi accountsApi = MockAccountsApi(); setupTestLocator((locator) { locator.registerLazySingleton(() => QuickNav()); @@ -130,7 +131,7 @@ void main() { expect(find.text(AppLocalizations().addStudent), findsOneWidget); // Tap the 'Add Student' button and wait for any transition animations to finish - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Check that PairingUtil was called @@ -142,8 +143,8 @@ void main() { var student1 = CanvasModelTestUtils.mockUser(name: 'Billy'); var callbackCalled = false; - PairingUtil pairingUtil = MockPairingUtil(); - AccountsApi accountsApi = MockAccountsApi(); + MockPairingUtil pairingUtil = MockPairingUtil(); + MockAccountsApi accountsApi = MockAccountsApi(); final analytics = _MockAnalytics(); when(pairingUtil.pairNewStudent(any, any)).thenAnswer((inv) => inv.positionalArguments[1]()); @@ -167,7 +168,7 @@ void main() { expect(find.text(AppLocalizations().addStudent), findsOneWidget); // Tap the 'Add Student' button and wait for any transition animations to finish - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Verify diff --git a/apps/flutter_parent/test/screens/events/event_details_interactor_test.dart b/apps/flutter_parent/test/screens/events/event_details_interactor_test.dart index fcdcfe680e..9a3a72e4a9 100644 --- a/apps/flutter_parent/test/screens/events/event_details_interactor_test.dart +++ b/apps/flutter_parent/test/screens/events/event_details_interactor_test.dart @@ -25,12 +25,13 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { // Setup - final eventsApi = _MockEventsApi(); - final reminderDb = _MockReminderDb(); - final notificationUtil = _MockNotificationUtil(); + final eventsApi = MockCalendarEventsApi(); + final reminderDb = MockReminderDb(); + final notificationUtil = MockNotificationUtil(); final login = Login((b) => b ..domain = 'test-domain' ..user = User((u) => u..id = '123').toBuilder()); @@ -130,10 +131,4 @@ void main() { verify(reminderDb.insert(reminder)); verify(notificationUtil.scheduleReminder(l10n, event.title, formattedDate, savedReminder)); }); -} - -class _MockEventsApi extends Mock implements CalendarEventsApi {} - -class _MockReminderDb extends Mock implements ReminderDb {} - -class _MockNotificationUtil extends Mock implements NotificationUtil {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/events/event_details_screen_test.dart b/apps/flutter_parent/test/screens/events/event_details_screen_test.dart index 502508df67..b771d744ac 100644 --- a/apps/flutter_parent/test/screens/events/event_details_screen_test.dart +++ b/apps/flutter_parent/test/screens/events/event_details_screen_test.dart @@ -35,6 +35,7 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import '../../utils/test_utils.dart'; void main() { @@ -59,8 +60,8 @@ void main() { ..date = DateTime(2100) ..userDomain = 'domain'); - final interactor = _MockEventDetailsInteractor(); - final mockNav = _MockQuickNav(); + final interactor = MockEventDetailsInteractor(); + final mockNav = MockQuickNav(); final l10n = AppLocalizations(); @@ -91,7 +92,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('shows error', (tester) async { - when(interactor.loadEvent(eventId, any)).thenAnswer((_) => Future.error('Failed to load event')); + when(interactor.loadEvent(eventId, any)).thenAnswer((_) => Future.error('Failed to load event')); await tester.pumpWidget(_testableWidget(EventDetailsScreen.withId(eventId: eventId))); await tester.pumpAndSettle(); // Let the future finish @@ -293,7 +294,7 @@ void main() { expect(find.text(l10n.eventRemindMeDescription), findsNothing); expect(find.text(l10n.eventRemindMeSet), findsOneWidget); expect((tester.widget(find.byType(Switch)) as Switch).value, true); - expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)), findsOneWidget); + expect(find.text(reminder.date.l10nFormat(AppLocalizations().dateAtTime)!), findsOneWidget); }); testWidgetsWithAccessibilityChecks('shows correct state when no reminder is set', (tester) async { @@ -476,8 +477,4 @@ Widget _testableWidget( eventDetailsScreen, platformConfig: config, ); -} - -class _MockEventDetailsInteractor extends Mock implements EventDetailsInteractor {} - -class _MockQuickNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart b/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart index e4f662629e..00178d1d4a 100644 --- a/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart +++ b/apps/flutter_parent/test/screens/help/help_screen_interactor_test.dart @@ -21,10 +21,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { test('getObserverCustomHelpLinks calls to HelpLinksApi', () async { - var api = _MockHelpLinksApi(); + var api = MockHelpLinksApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); when(api.getHelpLinks(forceRefresh: anyNamed('forceRefresh'))).thenAnswer((_) => Future.value(createHelpLinks())); @@ -34,7 +35,7 @@ void main() { }); test('getObserverCustomHelpLinks only returns links for observers', () async { - var api = _MockHelpLinksApi(); + var api = MockHelpLinksApi(); var customLinks = [ createHelpLink(availableTo: [AvailableTo.observer]), createHelpLink(availableTo: [AvailableTo.user]), @@ -96,7 +97,7 @@ void main() { }); test('custom list is returned if there are any custom lists', () async { - var api = _MockHelpLinksApi(); + var api = MockHelpLinksApi(); var customLinks = [ createHelpLink(availableTo: [AvailableTo.observer]), createHelpLink(availableTo: [AvailableTo.user]), @@ -118,7 +119,7 @@ void main() { }); test('default list is returned if there are no custom lists', () async { - var api = _MockHelpLinksApi(); + var api = MockHelpLinksApi(); var defaultLinks = [ createHelpLink(availableTo: [AvailableTo.user]), createHelpLink(availableTo: [AvailableTo.observer]), @@ -132,16 +133,14 @@ void main() { }); } -HelpLinks createHelpLinks({List customLinks, List defaultLinks}) => HelpLinks((b) => b +HelpLinks createHelpLinks({List? customLinks, List? defaultLinks}) => HelpLinks((b) => b ..customHelpLinks = ListBuilder(customLinks != null ? customLinks : [createHelpLink()]) ..defaultHelpLinks = ListBuilder(defaultLinks != null ? defaultLinks : [createHelpLink()])); -HelpLink createHelpLink({String id, String text, String url, List availableTo}) => HelpLink((b) => b +HelpLink createHelpLink({String? id, String? text, String? url, List? availableTo}) => HelpLink((b) => b ..id = id ?? '' ..type = '' ..availableTo = ListBuilder(availableTo != null ? availableTo : []) ..url = url ?? 'https://www.instructure.com' ..text = text ?? 'text' ..subtext = 'subtext'); - -class _MockHelpLinksApi extends Mock implements HelpLinksApi {} diff --git a/apps/flutter_parent/test/screens/help/help_screen_test.dart b/apps/flutter_parent/test/screens/help/help_screen_test.dart index bc5621eb43..4e941449fc 100644 --- a/apps/flutter_parent/test/screens/help/help_screen_test.dart +++ b/apps/flutter_parent/test/screens/help/help_screen_test.dart @@ -11,8 +11,6 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'dart:async'; - import 'package:built_collection/built_collection.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/help_link.dart'; @@ -20,7 +18,6 @@ import 'package:flutter_parent/models/login.dart'; import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/screens/help/help_screen.dart'; import 'package:flutter_parent/screens/help/help_screen_interactor.dart'; -import 'package:flutter_parent/screens/help/legal_screen.dart'; import 'package:flutter_parent/utils/common_widgets/error_report/error_report_dialog.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; @@ -31,12 +28,13 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final l10n = AppLocalizations(); - _MockUrlLauncher launcher = _MockUrlLauncher(); - _MockAndroidIntentVeneer intentVeneer = _MockAndroidIntentVeneer(); - HelpScreenInteractor interactor = _MockHelpScreenInteractor(); + MockUrlLauncher launcher = MockUrlLauncher(); + MockAndroidIntentVeneer intentVeneer = MockAndroidIntentVeneer(); + MockHelpScreenInteractor interactor = MockHelpScreenInteractor(); setupTestLocator((locator) { locator.registerSingleton(QuickNav()); @@ -63,9 +61,6 @@ void main() { expect(find.text(l10n.helpShareLoveLabel), findsOneWidget); expect(find.text(l10n.helpShareLoveDescription), findsOneWidget); - - expect(find.text(l10n.helpLegalLabel), findsOneWidget); - expect(find.text(l10n.helpLegalDescription), findsOneWidget); }); testWidgetsWithAccessibilityChecks('tapping search launches url', (tester) async { @@ -146,19 +141,6 @@ void main() { verify(intentVeneer.launchEmailWithBody(l10n.featureRequestSubject, emailBody)).called(1); }); - testWidgetsWithAccessibilityChecks('tapping legal shows legal screen', (tester) async { - when(interactor.getObserverCustomHelpLinks(forceRefresh: anyNamed('forceRefresh'))) - .thenAnswer((_) => Future.value([])); - - await tester.pumpWidget(TestApp(HelpScreen())); - await tester.pumpAndSettle(); - - await tester.tap(find.text(l10n.helpLegalLabel)); - await tester.pumpAndSettle(); - - expect(find.byType(LegalScreen), findsOneWidget); - }); - testWidgetsWithAccessibilityChecks('tapping telephone link launches correct intent', (tester) async { var telUri = 'tel:+123'; var text = 'Telephone'; @@ -223,16 +205,10 @@ void main() { }); } -HelpLink _createHelpLink({String id, String text, String url}) => HelpLink((b) => b +HelpLink _createHelpLink({String? id, String? text, String? url}) => HelpLink((b) => b ..id = id ?? '' ..type = '' ..availableTo = BuiltList.of([]).toBuilder() ..url = url ?? 'https://www.instructure.com' ..text = text ?? 'text' ..subtext = 'subtext'); - -class _MockUrlLauncher extends Mock implements UrlLauncher {} - -class _MockAndroidIntentVeneer extends Mock implements AndroidIntentVeneer {} - -class _MockHelpScreenInteractor extends Mock implements HelpScreenInteractor {} diff --git a/apps/flutter_parent/test/screens/help/terms_of_service_screen_test.dart b/apps/flutter_parent/test/screens/help/terms_of_service_screen_test.dart index 21fd3e715c..41d3865b83 100644 --- a/apps/flutter_parent/test/screens/help/terms_of_service_screen_test.dart +++ b/apps/flutter_parent/test/screens/help/terms_of_service_screen_test.dart @@ -27,11 +27,12 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { AppLocalizations l10n = AppLocalizations(); - final accountApi = _MockAccountApi(); + final accountApi = MockAccountsApi(); setupTestLocator((locator) { locator.registerLazySingleton(() => accountApi); @@ -86,6 +87,4 @@ void main() { expect(find.byType(WebView), findsOneWidget); }); -} - -class _MockAccountApi extends Mock implements AccountsApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_handler_test.dart b/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_handler_test.dart index 4e02a27573..06d88a921a 100644 --- a/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_handler_test.dart +++ b/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_handler_test.dart @@ -25,6 +25,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { setUpAll(() async { @@ -47,7 +48,7 @@ void main() { test('Calls onStageChange when stage changes', () { var handler = AttachmentHandler(File('')); - AttachmentUploadStage lastStage = null; + AttachmentUploadStage? lastStage = null; handler.onStageChange = (stage) => lastStage = stage; handler.stage = AttachmentUploadStage.CREATED; @@ -64,8 +65,8 @@ void main() { }); test('Notifies listeners during upload', () async { - final api = _MockFileUploadApi(); - final pathProvider = _MockPathProvider(); + final api = MockFileApi(); + final pathProvider = MockPathProviderVeneer(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); @@ -119,7 +120,7 @@ void main() { }); test('Sets failed state when API fails', () async { - final api = _MockFileUploadApi(); + final api = MockFileApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); when(api.uploadConversationFile(any, any)).thenAnswer((_) => Future.error('Error!')); @@ -130,7 +131,7 @@ void main() { }); test('performUpload does nothing if stage is uploading or finished', () async { - final api = _MockFileUploadApi(); + final api = MockFileApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); var handler = AttachmentHandler(File('')) @@ -182,7 +183,7 @@ void main() { }); test('cleans up file if local', () async { - final pathProvider = _MockPathProvider(); + final pathProvider = MockPathProviderVeneer(); await setupTestLocator((locator) { locator.registerLazySingleton(() => pathProvider); @@ -205,7 +206,7 @@ void main() { }); test('does not clean up file if not local', () async { - final pathProvider = _MockPathProvider(); + final pathProvider = MockPathProviderVeneer(); await setupTestLocator((locator) { locator.registerLazySingleton(() => pathProvider); @@ -228,7 +229,7 @@ void main() { }); test('cleanUpFile prints error on failure', interceptPrint((log) async { - final pathProvider = _MockPathProvider(); + final pathProvider = MockPathProviderVeneer(); await setupTestLocator((locator) { locator.registerLazySingleton(() => pathProvider); @@ -244,7 +245,7 @@ void main() { })); test('deleteAttachment calls API if attachment exists', () async { - final api = _MockFileUploadApi(); + final api = MockFileApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); when(api.deleteFile(any)).thenAnswer((_) async {}); @@ -255,7 +256,7 @@ void main() { }); test('deleteAttachment does not call API if attachment is null', () async { - final api = _MockFileUploadApi(); + final api = MockFileApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); var handler = AttachmentHandler(null); @@ -265,7 +266,7 @@ void main() { }); test('deleteAttachment prints error on failure', interceptPrint((log) async { - final api = _MockFileUploadApi(); + final api = MockFileApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); when(api.deleteFile(any)).thenAnswer((_) => Future.error(Error())); @@ -282,7 +283,3 @@ interceptPrint(testBody(List log)) => () { final spec = ZoneSpecification(print: (self, parent, zone, String msg) => log.add(msg)); return Zone.current.fork(specification: spec).run(() => testBody(log)); }; - -class _MockFileUploadApi extends Mock implements FileApi {} - -class _MockPathProvider extends Mock implements PathProviderVeneer {} diff --git a/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_picker_test.dart b/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_picker_test.dart index fcc0fae3c3..baf6d1f162 100644 --- a/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_picker_test.dart +++ b/apps/flutter_parent/test/screens/inbox/attachment_utils/attachment_picker_test.dart @@ -26,12 +26,13 @@ import 'package:mockito/mockito.dart'; import '../../../utils/accessibility_utils.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final l10n = AppLocalizations(); testWidgetsWithAccessibilityChecks('Tapping camera option shows preparing UI', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); when(interactor.getImageFromCamera()).thenAnswer((_) => Completer().future); @@ -48,7 +49,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping gallery option shows preparing UI', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); when(interactor.getImageFromGallery()).thenAnswer((_) => Completer().future); @@ -65,7 +66,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping file option shows preparing UI', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); when(interactor.getFileFromDevice()).thenAnswer((_) => Completer().future); @@ -82,7 +83,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping camera option invokes interactor methods', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); await tester.pumpWidget(TestApp(AttachmentPicker())); @@ -96,7 +97,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping gallery option invokes interactor methods', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); await tester.pumpWidget(TestApp(AttachmentPicker())); @@ -110,7 +111,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping file option invokes interactor methods', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); await tester.pumpWidget(TestApp(AttachmentPicker())); @@ -124,10 +125,10 @@ void main() { }); testWidgetsWithAccessibilityChecks('Canceling camera option returns to picker ui', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); - final completer = Completer(); + final completer = Completer(); when(interactor.getImageFromCamera()).thenAnswer((_) => completer.future); await tester.pumpWidget(TestApp(AttachmentPicker())); @@ -154,10 +155,10 @@ void main() { }); testWidgetsWithAccessibilityChecks('Canceling gallery option returns to picker ui', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); - final completer = Completer(); + final completer = Completer(); when(interactor.getImageFromGallery()).thenAnswer((_) => completer.future); await tester.pumpWidget(TestApp(AttachmentPicker())); @@ -184,10 +185,10 @@ void main() { }); testWidgetsWithAccessibilityChecks('Canceling file option returns to picker ui', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); - final completer = Completer(); + final completer = Completer(); when(interactor.getFileFromDevice()).thenAnswer((_) => completer.future); await tester.pumpWidget(TestApp(AttachmentPicker())); @@ -214,17 +215,18 @@ void main() { }); testWidgets('Returns non-null result when successful', (tester) async { - final interactor = _MockInteractor(); + final interactor = MockAttachmentPickerInteractor(); _setupLocator(interactor); final file = File('/fake/path'); when(interactor.getFileFromDevice()).thenAnswer((_) => Future.value(file)); - AttachmentHandler result = null; + AttachmentHandler? result = null; await tester.pumpWidget(TestApp(Builder( builder: (context) => Container( - child: RaisedButton( + child: ElevatedButton( + child: Container(), onPressed: () async { result = await CreateConversationInteractor().addAttachment(context); }, @@ -233,20 +235,18 @@ void main() { ))); await tester.pump(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Tap file option await tester.tap(find.text(l10n.uploadFile)); await tester.pumpAndSettle(); - expect(find.byType(RaisedButton), findsOneWidget); + expect(find.byType(ElevatedButton), findsOneWidget); expect(result, isNotNull); }); } -_setupLocator(_MockInteractor interactor) => +_setupLocator(MockAttachmentPickerInteractor interactor) => setupTestLocator((locator) => locator.registerFactory(() => interactor)); - -class _MockInteractor extends Mock implements AttachmentPickerInteractor {} diff --git a/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_interactor_test.dart b/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_interactor_test.dart index fe60420ae4..4a4937ca50 100644 --- a/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_interactor_test.dart +++ b/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_interactor_test.dart @@ -29,15 +29,16 @@ import 'package:test/test.dart'; import '../../../utils/platform_config.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { test('getConversation calls InboxApi with correct parameters', () async { final conversationId = '123'; - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); - locator.registerLazySingleton(() => _MockInboxNotifier()); + locator.registerLazySingleton(() => MockInboxCountNotifier()); }); await ConversationDetailsInteractor().getConversation(conversationId); @@ -45,8 +46,8 @@ void main() { }); test('getConversation updates InboxCountNotifier when successful', () async { - var api = _MockInboxApi(); - var notifier = _MockInboxNotifier(); + var api = MockInboxApi(); + var notifier = MockInboxCountNotifier(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); @@ -72,7 +73,7 @@ void main() { }); test('addReply calls QuickNav with correct parameters', () async { - var nav = _MockNav(); + var nav = MockQuickNav(); await setupTestLocator((locator) { locator.registerLazySingleton(() => nav); }); @@ -80,7 +81,7 @@ void main() { Conversation conversation = Conversation(); Message message = Message(); bool replyAll = true; - BuildContext context = _MockContext(); + BuildContext context = MockBuildContext(); await ConversationDetailsInteractor().addReply(context, conversation, message, replyAll); var verification = verify(nav.push(context, captureAny)); @@ -95,13 +96,13 @@ void main() { }); test('viewAttachment calls QuickNav with correct parameters', () async { - var nav = _MockNav(); + var nav = MockQuickNav(); await setupTestLocator((locator) { locator.registerLazySingleton(() => nav); }); Attachment attachment = Attachment(); - BuildContext context = _MockContext(); + BuildContext context = MockBuildContext(); await ConversationDetailsInteractor().viewAttachment(context, attachment); var verification = verify(nav.push(context, captureAny)); @@ -110,12 +111,4 @@ void main() { expect(verification.captured[0], isA()); expect((verification.captured[0] as ViewAttachmentScreen).attachment, attachment); }); -} - -class _MockNav extends Mock implements QuickNav {} - -class _MockContext extends Mock implements BuildContext {} - -class _MockInboxApi extends Mock implements InboxApi {} - -class _MockInboxNotifier extends Mock implements InboxCountNotifier {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_screen_test.dart b/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_screen_test.dart index 480fbbde03..9bd4cd6344 100644 --- a/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_screen_test.dart +++ b/apps/flutter_parent/test/screens/inbox/conversation_details/conversation_details_screen_test.dart @@ -37,6 +37,7 @@ import '../../../utils/accessibility_utils.dart'; import '../../../utils/finders.dart'; import '../../../utils/network_image_response.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { mockNetworkImageResponse(); @@ -45,7 +46,7 @@ void main() { group('Displays messages', () { testWidgetsWithAccessibilityChecks('displays single message', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var conversation = Conversation((c) => c @@ -76,11 +77,11 @@ void main() { var messageWidget = find.byType(MessageWidget); expect(messageWidget, findsOneWidget); expect( - find.descendant(of: messageWidget, matching: find.richText(conversation.messages[0].body)), findsOneWidget); + find.descendant(of: messageWidget, matching: find.richText(conversation.messages![0].body!)), findsOneWidget); }); testWidgetsWithAccessibilityChecks('displays multiple messages in correct order', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var conversation = Conversation((c) => c @@ -130,8 +131,8 @@ void main() { expect(find.byType(MessageWidget).evaluate().length, 3); // Get widget positions in the same order as the messages in the conversation - var messageWidgetOffsets = conversation.messages - .map((it) => find.ancestor(of: find.richText(it.body), matching: find.byType(MessageWidget))) + var messageWidgetOffsets = conversation.messages! + .map((it) => find.ancestor(of: find.richText(it.body!), matching: find.byType(MessageWidget))) .map((it) => tester.getTopLeft(it)) .toList(); @@ -146,7 +147,7 @@ void main() { group('Displays base details', () { testWidgetsWithAccessibilityChecks('loading state', (tester) async { final subject = 'This is a subject'; - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); Completer completer = Completer(); @@ -162,7 +163,7 @@ void main() { testWidgetsWithAccessibilityChecks('conversation subject', (tester) async { final subject = 'This is a subject'; - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(Conversation())); @@ -176,7 +177,7 @@ void main() { testWidgetsWithAccessibilityChecks('course name', (tester) async { final courseName = 'BIO 101'; - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(Conversation())); @@ -189,7 +190,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('reply FAB', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(Conversation())); @@ -202,7 +203,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('error state', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.error('')); @@ -224,7 +225,7 @@ void main() { group('interactions', () { testWidgetsWithAccessibilityChecks('Displays conversation reply options', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(Conversation())); @@ -249,7 +250,7 @@ void main() { ..jsonId = JsonObject('1') ..displayName = 'Attachment 1'), ]; - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var conversation = Conversation((c) => c @@ -287,7 +288,7 @@ void main() { testWidgetsWithAccessibilityChecks('Replying to conversation calls interactor', (tester) async { final conversation = Conversation(); - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(conversation)); @@ -310,7 +311,7 @@ void main() { testWidgetsWithAccessibilityChecks('Replying all to conversation calls interactor', (tester) async { final conversation = Conversation(); - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(conversation)); @@ -332,7 +333,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Swiping message reveals reply options', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var conversation = Conversation((c) => c @@ -367,7 +368,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Replying to individual message calls interactor', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var conversation = Conversation((c) => c @@ -400,11 +401,11 @@ void main() { await tester.tap(find.text(l10n.reply)); await tester.pumpAndSettle(); - verify(interactor.addReply(any, conversation, conversation.messages[0], false)).called(1); + verify(interactor.addReply(any, conversation, conversation.messages![0], false)).called(1); }); testWidgetsWithAccessibilityChecks('Replying all to individual message calls interactor', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var conversation = Conversation((c) => c @@ -437,12 +438,12 @@ void main() { await tester.tap(find.text(l10n.replyAll)); await tester.pumpAndSettle(); - verify(interactor.addReply(any, conversation, conversation.messages[0], true)).called(1); + verify(interactor.addReply(any, conversation, conversation.messages![0], true)).called(1); }); testWidgetsWithAccessibilityChecks('error state refresh calls interactor', (tester) async { final conversationId = '100'; - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.error('')); @@ -465,7 +466,7 @@ void main() { testWidgetsWithAccessibilityChecks('pull-to-refresh calls interactor', (tester) async { final conversationId = '100'; - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversation(any)).thenAnswer((_) => Future.value(Conversation())); @@ -485,7 +486,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('updates after adding message', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var message1Body = 'Original message'; @@ -537,7 +538,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('route returns true if conversation was updated', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -550,7 +551,7 @@ void main() { TestApp( Builder( builder: (context) => Material( - child: FlatButton( + child: TextButton( child: Text('Click me'), onPressed: () async { returnValue = await QuickNav().push( @@ -579,7 +580,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('route returns false if conversation was not updated', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationDetailsInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -591,7 +592,7 @@ void main() { TestApp( Builder( builder: (context) => Material( - child: FlatButton( + child: TextButton( child: Text('Click me'), onPressed: () async { returnValue = await QuickNav().push( @@ -616,5 +617,3 @@ void main() { }); }); } - -class _MockInteractor extends Mock implements ConversationDetailsInteractor {} diff --git a/apps/flutter_parent/test/screens/inbox/conversation_details/message_widget_test.dart b/apps/flutter_parent/test/screens/inbox/conversation_details/message_widget_test.dart index defb42146e..5bb6a2f50d 100644 --- a/apps/flutter_parent/test/screens/inbox/conversation_details/message_widget_test.dart +++ b/apps/flutter_parent/test/screens/inbox/conversation_details/message_widget_test.dart @@ -29,6 +29,7 @@ import '../../../utils/accessibility_utils.dart'; import '../../../utils/finders.dart'; import '../../../utils/network_image_response.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { mockNetworkImageResponse(); @@ -61,7 +62,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Me'); + expect(widget.textSpan?.toPlainText(), 'Me'); }); testWidgetsWithAccessibilityChecks('for message to one other', (tester) async { @@ -92,7 +93,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Me to User 1'); + expect(widget.textSpan?.toPlainText(), 'Me to User 1'); }); testWidgetsWithAccessibilityChecks('for message to multiple others', (tester) async { @@ -132,7 +133,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Me to 4 others'); + expect(widget.textSpan?.toPlainText(), 'Me to 4 others'); }); testWidgetsWithAccessibilityChecks('expands to show participant info', (tester) async { @@ -249,7 +250,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'User 1 to me'); + expect(widget.textSpan?.toPlainText(), 'User 1 to me'); }); testWidgetsWithAccessibilityChecks('for message from another to multiple others', (tester) async { @@ -289,7 +290,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'User 1 to me & 3 others'); + expect(widget.textSpan?.toPlainText(), 'User 1 to me & 3 others'); }); testWidgetsWithAccessibilityChecks('with pronoun', (tester) async { @@ -321,7 +322,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'User 1 (pro/noun) to me'); + expect(widget.textSpan?.toPlainText(), 'User 1 (pro/noun) to me'); }); testWidgetsWithAccessibilityChecks('for message to one other with pronouns', (tester) async { @@ -353,7 +354,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Me to User 1 (pro/noun)'); + expect(widget.textSpan?.toPlainText(), 'Me to User 1 (pro/noun)'); }); testWidgetsWithAccessibilityChecks('for message to unknown user', (tester) async { @@ -381,7 +382,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Me to Unknown User'); + expect(widget.textSpan?.toPlainText(), 'Me to Unknown User'); }); testWidgetsWithAccessibilityChecks('for message from unknown user', (tester) async { @@ -409,7 +410,7 @@ void main() { expect(find.byKey(Key('author-info')), findsOneWidget); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Unknown User to me'); + expect(widget.textSpan?.toPlainText(), 'Unknown User to me'); }); }); @@ -444,7 +445,7 @@ void main() { await tester.pumpAndSettle(); var widget = find.byKey(Key('author-info')).evaluate().first.widget as Text; - expect(widget.textSpan.toPlainText(), 'Me to $longUserName'); + expect(widget.textSpan?.toPlainText(), 'Me to $longUserName'); // At this point the test should have succeeded without throwing an overflow error }); @@ -538,12 +539,12 @@ void main() { var attachment1 = find.byKey(Key('attachment-1')); expect(attachment1, findsOneWidget); - expect(find.descendant(of: attachment1, matching: find.text(attachments[0].displayName)), findsOneWidget); + expect(find.descendant(of: attachment1, matching: find.text(attachments[0].displayName!)), findsOneWidget); expect(find.descendant(of: attachment1, matching: find.byType(FadeInImage)), findsNothing); var attachment2 = find.byKey(Key('attachment-2')); expect(attachment2, findsOneWidget); - expect(find.descendant(of: attachment2, matching: find.text(attachments[1].displayName)), findsOneWidget); + expect(find.descendant(of: attachment2, matching: find.text(attachments[1].displayName!)), findsOneWidget); expect(find.descendant(of: attachment2, matching: find.byType(FadeInImage)), findsOneWidget); }); @@ -579,7 +580,7 @@ void main() { var attachment1 = find.byKey(Key('attachment-media-comment-fake-id')); expect(attachment1, findsOneWidget); - expect(find.descendant(of: attachment1, matching: find.text(mediaComment.displayName)), findsOneWidget); + expect(find.descendant(of: attachment1, matching: find.text(mediaComment.displayName!)), findsOneWidget); }); }); @@ -604,7 +605,7 @@ void main() { ..name = 'Myself'), ])); - Attachment actual = null; + Attachment? actual = null; await tester.pumpWidget( TestApp( @@ -676,7 +677,7 @@ void main() { testWidgetsWithAccessibilityChecks( 'links are selectable', (tester) async { - final nav = _MockNav(); + final nav = MockQuickNav(); setupTestLocator((locator) => locator.registerLazySingleton(() => nav)); final url = 'https://www.google.com'; @@ -711,6 +712,4 @@ void main() { a11yExclusions: {A11yExclusion.minTapSize}, skip: true // inline links are not required to meet the min tap target size ); }); -} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_interactor_test.dart b/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_interactor_test.dart index 9534d73ad9..3d4ed507ee 100644 --- a/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_interactor_test.dart +++ b/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_interactor_test.dart @@ -28,10 +28,11 @@ import 'package:tuple/tuple.dart'; import '../../../utils/test_app.dart'; import '../../../utils/test_helpers/mock_helpers.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { test('getConversations calls api for normal scope and sent scope', () async { - var inboxApi = _MockInboxApi(); + var inboxApi = MockInboxApi(); when(inboxApi.getConversations( scope: anyNamed('scope'), @@ -49,7 +50,7 @@ void main() { }); test('getConversations merges scopes and removes duplicates from sent scope', () async { - var inboxApi = _MockInboxApi(); + var inboxApi = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => inboxApi); @@ -95,7 +96,7 @@ void main() { }); test('getConversations orders items by date (descending)', () async { - var inboxApi = _MockInboxApi(); + var inboxApi = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => inboxApi); @@ -143,7 +144,7 @@ void main() { }); test('getConversations produces error when API fails', () async { - var inboxApi = _MockInboxApi(); + var inboxApi = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => inboxApi); @@ -164,7 +165,7 @@ void main() { }); test('getCoursesForCompose calls CourseApi', () async { - var api = _MockCourseApi(); + var api = MockCourseApi(); await setupTestLocator((locator) => locator.registerLazySingleton(() => api)); ConversationListInteractor().getCoursesForCompose(); verify(api.getObserveeCourses()).called(1); @@ -202,13 +203,13 @@ void main() { ..enrollments = ListBuilder(enrollments.where((e) => e.courseId == choir))); List> expectedResult = [ - Tuple2(andyEnrollments[0].observedUser, boxingCourse), - Tuple2(andyEnrollments[1].observedUser, choirCourse), - Tuple2(billEnrollments[1].observedUser, arithmeticCourse), - Tuple2(billEnrollments[0].observedUser, choirCourse), - Tuple2(chericeEnrollments[1].observedUser, arithmeticCourse), - Tuple2(chericeEnrollments[2].observedUser, boxingCourse), - Tuple2(chericeEnrollments[0].observedUser, choirCourse), + Tuple2(andyEnrollments[0].observedUser!, boxingCourse), + Tuple2(andyEnrollments[1].observedUser!, choirCourse), + Tuple2(billEnrollments[1].observedUser!, arithmeticCourse), + Tuple2(billEnrollments[0].observedUser!, choirCourse), + Tuple2(chericeEnrollments[1].observedUser!, arithmeticCourse), + Tuple2(chericeEnrollments[2].observedUser!, boxingCourse), + Tuple2(chericeEnrollments[0].observedUser!, choirCourse), ]; List> actual = ConversationListInteractor() @@ -247,13 +248,13 @@ void main() { ..enrollments = ListBuilder(enrollments.where((e) => e.courseId == choir))); List> expectedResult = [ - Tuple2(andyEnrollments[0].observedUser, boxingCourse), - Tuple2(andyEnrollments[1].observedUser, choirCourse), - Tuple2(billEnrollments[1].observedUser, arithmeticCourse), - Tuple2(billEnrollments[0].observedUser, choirCourse), - Tuple2(chericeEnrollments[1].observedUser, arithmeticCourse), - Tuple2(chericeEnrollments[2].observedUser, boxingCourse), - Tuple2(chericeEnrollments[0].observedUser, choirCourse), + Tuple2(andyEnrollments[0].observedUser!, boxingCourse), + Tuple2(andyEnrollments[1].observedUser!, choirCourse), + Tuple2(billEnrollments[1].observedUser!, arithmeticCourse), + Tuple2(billEnrollments[0].observedUser!, choirCourse), + Tuple2(chericeEnrollments[1].observedUser!, arithmeticCourse), + Tuple2(chericeEnrollments[2].observedUser!, boxingCourse), + Tuple2(chericeEnrollments[0].observedUser!, choirCourse), ]; List> actual = ConversationListInteractor() @@ -290,13 +291,13 @@ void main() { ..enrollments = ListBuilder(enrollments.where((e) => e.courseId == choir))); List> expected = [ - Tuple2(andyEnrollments[0].observedUser, boxingCourse), - Tuple2(andyEnrollments[1].observedUser, choirCourse), - Tuple2(billEnrollments[1].observedUser, arithmeticCourse), - Tuple2(billEnrollments[0].observedUser, choirCourse), - Tuple2(chericeEnrollments[1].observedUser, arithmeticCourse), - Tuple2(chericeEnrollments[2].observedUser, boxingCourse), - Tuple2(chericeEnrollments[0].observedUser, choirCourse), + Tuple2(andyEnrollments[0].observedUser!, boxingCourse), + Tuple2(andyEnrollments[1].observedUser!, choirCourse), + Tuple2(billEnrollments[1].observedUser!, arithmeticCourse), + Tuple2(billEnrollments[0].observedUser!, choirCourse), + Tuple2(chericeEnrollments[1].observedUser!, arithmeticCourse), + Tuple2(chericeEnrollments[2].observedUser!, boxingCourse), + Tuple2(chericeEnrollments[0].observedUser!, choirCourse), ]; List> actual = ConversationListInteractor().combineEnrollmentsAndCourses( @@ -317,8 +318,4 @@ List _createEnrollments(String studentName, List courseIds) ..id = studentName).toBuilder() ..courseId = id)) .toList(); -} - -class _MockInboxApi extends Mock implements InboxApi {} - -class _MockCourseApi extends Mock implements CourseApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_screen_test.dart b/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_screen_test.dart index e4a32d9473..4a9bffaff5 100644 --- a/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_screen_test.dart +++ b/apps/flutter_parent/test/screens/inbox/conversation_list/conversation_list_screen_test.dart @@ -35,13 +35,14 @@ import 'package:mockito/mockito.dart'; import '../../../utils/accessibility_utils.dart'; import '../../../utils/network_image_response.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { mockNetworkImageResponse(); final l10n = AppLocalizations(); testWidgetsWithAccessibilityChecks('Displays loading state', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var completer = Completer>(); @@ -55,7 +56,7 @@ void main() { // TODO Fix test testWidgetsWithAccessibilityChecks('Displays empty state', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversations()).thenAnswer((_) => Future.value([])); @@ -70,7 +71,7 @@ void main() { // TODO Fix test testWidgetsWithAccessibilityChecks('Displays error state with retry', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversations()).thenAnswer((_) => Future.error('Error')); @@ -82,7 +83,7 @@ void main() { var errorIcon = await tester.widget(find.byType(Icon).first).icon; expect(errorIcon, equals(CanvasIcons.warning)); expect(find.text('There was an error loading your inbox messages.'), findsOneWidget); - expect(find.widgetWithText(FlatButton, l10n.retry), findsOneWidget); + expect(find.widgetWithText(TextButton, l10n.retry), findsOneWidget); // Retry with success reset(interactor); @@ -92,11 +93,11 @@ void main() { // Should no longer show error state expect(find.text('There was an error loading your inbox messages.'), findsNothing); - expect(find.widgetWithText(FlatButton, l10n.retry), findsNothing); + expect(find.widgetWithText(TextButton, l10n.retry), findsNothing); }, skip: true); testWidgetsWithAccessibilityChecks('Displays subject, course name, message preview, and date', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -113,13 +114,13 @@ void main() { await tester.pumpAndSettle(); expect(find.text(conversation.subject), findsOneWidget); - expect(find.text(conversation.contextName), findsOneWidget); - expect(find.text(conversation.lastMessage), findsOneWidget); + expect(find.text(conversation.contextName!), findsOneWidget); + expect(find.text(conversation.lastMessage!), findsOneWidget); expect(find.text('Dec 25'), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Adds year to date if not this year', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -138,7 +139,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays time if date is today', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -156,7 +157,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays single avatar for single participant', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -179,7 +180,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays double avatar for two participants', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -204,7 +205,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays group icon for more than two participants', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -232,7 +233,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping add button shows messageable course list', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); when(interactor.getConversations()).thenAnswer((_) => Future.error('')); @@ -246,7 +247,7 @@ void main() { when(interactor.getCoursesForCompose()).thenAnswer((_) => courseCompleter.future); when(interactor.getStudentEnrollments()).thenAnswer((_) => enrollmentCompleter.future); - await tester.tap(find.bySemanticsLabel(l10n.newMessageTitle)); + await tester.tap(find.byTooltip(l10n.newMessageTitle)); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); @@ -290,7 +291,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Shows error in messegable course list', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var completer = Completer>(); @@ -300,7 +301,7 @@ void main() { await tester.pumpWidget(TestApp(ConversationListScreen())); await tester.pumpAndSettle(); - await tester.tap(find.bySemanticsLabel(l10n.newMessageTitle)); + await tester.tap(find.byTooltip(l10n.newMessageTitle)); await tester.pump(); completer.completeError(''); @@ -310,7 +311,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays unread message indicator', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockConversationListInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); var now = DateTime.now(); @@ -332,8 +333,8 @@ void main() { }); testWidgetsWithAccessibilityChecks('Refreshes on new message created', (tester) async { - var interactor = _MockInteractor(); - var nav = _MockNav(); + var interactor = MockConversationListInteractor(); + var nav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => nav); locator.registerFactory(() => interactor); @@ -350,7 +351,7 @@ void main() { when(interactor.getCoursesForCompose()).thenAnswer((_) => courseCompleter.future); when(interactor.getStudentEnrollments()).thenAnswer((_) => enrollmentCompleter.future); - await tester.tap(find.bySemanticsLabel(l10n.newMessageTitle)); + await tester.tap(find.byTooltip(l10n.newMessageTitle)); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); @@ -381,12 +382,12 @@ void main() { await tester.tap(find.text('Course 1')); await tester.pumpAndSettle(Duration(seconds: 1)); - expect(find.text(conversation.lastMessage), findsOneWidget); + expect(find.text(conversation.lastMessage!), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Refreshes on conversation updated', (tester) async { - var interactor = _MockInteractor(); - var nav = _MockNav(); + var interactor = MockConversationListInteractor(); + var nav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => nav); locator.registerFactory(() => interactor); @@ -405,7 +406,7 @@ void main() { await tester.pumpWidget(TestApp(ConversationListScreen())); await tester.pumpAndSettle(); - expect(find.text(conversation.lastMessage), findsOneWidget); + expect(find.text(conversation.lastMessage!), findsOneWidget); expect(find.byKey(Key('unread-indicator')), findsOneWidget); final updatedConversation = conversation.rebuild((b) => b @@ -414,14 +415,10 @@ void main() { when(interactor.getConversations(forceRefresh: anyNamed('forceRefresh'))) .thenAnswer((_) => Future.value([updatedConversation])); - await tester.tap(find.text(conversation.lastMessage)); + await tester.tap(find.text(conversation.lastMessage!)); await tester.pumpAndSettle(); - expect(find.text(updatedConversation.lastMessage), findsOneWidget); + expect(find.text(updatedConversation.lastMessage!), findsOneWidget); expect(find.byKey(Key('unread-indicator')), findsNothing); }); -} - -class _MockInteractor extends Mock implements ConversationListInteractor {} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_interactor_test.dart b/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_interactor_test.dart index f00f9c6d5d..7fc53218ed 100644 --- a/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_interactor_test.dart +++ b/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_interactor_test.dart @@ -26,13 +26,14 @@ import 'package:mockito/mockito.dart'; import '../../../utils/canvas_model_utils.dart'; import '../../../utils/platform_config.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { String studentId = 'student_123'; var user = CanvasModelTestUtils.mockUser(id: 'user_123'); - final inboxApi = _MockInboxApi(); - final courseApi = _MockCourseApi(); + final inboxApi = MockInboxApi(); + final courseApi = MockCourseApi(); setupTestLocator((locator) { locator.registerLazySingleton(() => inboxApi); @@ -184,8 +185,4 @@ void main() { expect(actual.recipients, expectedRecipients); }); -} - -class _MockInboxApi extends Mock implements InboxApi {} - -class _MockCourseApi extends Mock implements CourseApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_screen_test.dart b/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_screen_test.dart index e67ad4a34b..b4fdf189ce 100644 --- a/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_screen_test.dart +++ b/apps/flutter_parent/test/screens/inbox/create_conversation/create_conversation_screen_test.dart @@ -33,6 +33,7 @@ import 'package:mockito/mockito.dart'; import '../../../utils/accessibility_utils.dart'; import '../../../utils/network_image_response.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; import '../../../utils/test_utils.dart'; /** @@ -48,17 +49,17 @@ void main() { _setupLocator( {int recipientCount = 4, - AttachmentHandler attachmentHandler, - int fetchFailCount: 0, - int sendFailCount: 0, - bool pronouns: false}) async { + AttachmentHandler? attachmentHandler, + int fetchFailCount = 0, + int sendFailCount = 0, + bool pronouns = false}) async { await setupTestLocator((locator) { locator.registerFactory( () => _MockInteractor(recipientCount, attachmentHandler, fetchFailCount, sendFailCount, pronouns)); }); } - Widget _testableWidget({Course course, String subject, String postscript, MockNavigatorObserver observer}) { + Widget _testableWidget({Course? course, String? subject, String? postscript, MockNavigatorObserver? observer}) { if (course == null) course = _mockCourse('0'); return TestApp( CreateConversationScreen(course.id, studentId, subject ?? course.name, postscript), @@ -241,7 +242,7 @@ void main() { await tester.pumpAndSettle(); var matchedWidget = find.byKey(CreateConversationScreen.subjectKey); - expect(tester.widget(matchedWidget).controller.text, course.name); + expect(tester.widget(matchedWidget).controller!.text, course.name); }); testWidgetsWithAccessibilityChecks('subject can be edited', (tester) async { @@ -252,10 +253,10 @@ void main() { await tester.pumpAndSettle(); var matchedWidget = find.byKey(CreateConversationScreen.subjectKey); - await tester.enterText(matchedWidget, course.courseCode); + await tester.enterText(matchedWidget, course.courseCode!); await tester.pump(); - expect(tester.widget(matchedWidget).controller.text, course.courseCode); + expect(tester.widget(matchedWidget).controller!.text, course.courseCode); }); testWidgetsWithAccessibilityChecks('prepopulates recipients', (tester) async { @@ -363,7 +364,7 @@ void main() { final course = _mockCourse('0'); // Set up attachment handler in 'uploading' stage - var handler = _MockedAttachmentHandler(); + var handler = MockAttachmentHandler(); when(handler.stage).thenReturn(AttachmentUploadStage.FINISHED); when(handler.attachment).thenReturn(Attachment((b) => b ..displayName = 'File' @@ -535,7 +536,7 @@ void main() { testWidgetsWithAccessibilityChecks('deleting an attachment calls AttachmentHandler.deleteAttachment', (tester) async { // Set up attachment handler in 'uploading' stage - var handler = _MockedAttachmentHandler(); + var handler = MockAttachmentHandler(); when(handler.stage).thenReturn(AttachmentUploadStage.FINISHED); when(handler.attachment).thenReturn(Attachment((b) => b ..displayName = 'File' @@ -575,6 +576,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(CreateConversationScreen.attachmentKey)); await tester.pump(); + await tester.pumpAndSettle(); // Assert attachment widget is displayed var attachmentWidget = find.byType(AttachmentWidget); @@ -582,6 +584,7 @@ void main() { await tester.longPress(attachmentWidget); await tester.pump(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); expect(find.text('file.txt'), findsOneWidget); }); @@ -597,6 +600,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(CreateConversationScreen.attachmentKey)); await tester.pump(); + await tester.pumpAndSettle(); // Assert attachment widget is displayed var attachmentWidget = find.byType(AttachmentWidget); @@ -604,6 +608,7 @@ void main() { await tester.longPress(attachmentWidget); await tester.pump(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); expect(find.text('file.txt'), findsOneWidget); }); @@ -621,6 +626,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(CreateConversationScreen.attachmentKey)); await tester.pump(); + await tester.pumpAndSettle(); // Assert attachment widget is displayed var attachmentWidget = find.byType(AttachmentWidget); @@ -628,6 +634,7 @@ void main() { await tester.longPress(attachmentWidget); await tester.pump(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); expect(find.text('upload.txt'), findsNWidgets(2)); // 2 widgets: one is the tooltip and one is the regular label }); @@ -807,7 +814,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays enrollment types', (tester) async { - var interactor = _MockedInteractor(); + var interactor = MockCreateConversationInteractor(); await GetIt.instance.reset(); GetIt.instance.registerFactory(() => interactor); @@ -885,7 +892,7 @@ void main() { })); } - var interactor = _MockedInteractor(); + var interactor = MockCreateConversationInteractor(); final data = CreateConversationData(course, [_makeRecipient('123', 'TeacherEnrollment')]); when(interactor.loadData(any, any)).thenAnswer((_) async => data); await GetIt.instance.reset(); @@ -921,13 +928,11 @@ void main() { }); } -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - /// Load up a temp page with a button to navigate to our screen, that way the back button exists in the app bar -Future _pumpTestableWidgetWithBackButton(tester, Widget widget, {MockNavigatorObserver observer}) async { +Future _pumpTestableWidgetWithBackButton(tester, Widget widget, {MockNavigatorObserver? observer}) async { final app = TestApp( Builder( - builder: (context) => FlatButton( + builder: (context) => TextButton( child: Semantics(label: 'test', child: const SizedBox()), onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => widget)), ), @@ -937,19 +942,17 @@ Future _pumpTestableWidgetWithBackButton(tester, Widget widget, {MockNavig await tester.pumpWidget(app); await tester.pumpAndSettle(); - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); if (observer != null) { verify(observer.didPush(any, any)).called(2); // Twice, first for the initial page, then for the navigator route } } -class _MockedInteractor extends Mock implements CreateConversationInteractor {} - class _MockInteractor extends CreateConversationInteractor { final _recipientCount; - final AttachmentHandler mockAttachmentHandler; + final AttachmentHandler? mockAttachmentHandler; int _fetchFailCount; @@ -961,7 +964,7 @@ class _MockInteractor extends CreateConversationInteractor { [this._pronouns = false]); @override - Future loadData(String courseId, String studentId) async { + Future loadData(String courseId, String? studentId) async { if (_fetchFailCount > 0) { _fetchFailCount--; return Future.error('Error!'); @@ -1005,8 +1008,6 @@ class _MockInteractor extends CreateConversationInteractor { } } -class _MockedAttachmentHandler extends Mock implements AttachmentHandler {} - class _MockAttachmentHandler extends AttachmentHandler { _MockAttachmentHandler() : super(null); diff --git a/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_interactor_test.dart b/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_interactor_test.dart index a7a59e0aec..d736ce9297 100644 --- a/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_interactor_test.dart +++ b/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_interactor_test.dart @@ -30,6 +30,7 @@ import 'package:test/test.dart'; import '../../../utils/platform_config.dart'; import '../../../utils/test_app.dart'; import '../../../utils/test_helpers/mock_helpers.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { setUp(() async { @@ -44,7 +45,7 @@ void main() { group('createReply calls InboxApi with correct params', () { test('for conversation reply', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); }); @@ -62,7 +63,7 @@ void main() { }); test('for conversation reply all', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); var enrollmentsApi = MockEnrollmentsApi(); var courseApi = MockCourseApi(); await setupTestLocator((locator) { @@ -89,7 +90,7 @@ void main() { }); test('for message reply', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); }); @@ -100,7 +101,7 @@ void main() { String body = 'This is a reply'; List attachmentIds = ['a', 'b', 'c']; bool replyAll = false; - Message message = conversation.messages[1]; + Message message = conversation.messages![1]; interactor.createReply(conversation, message, body, attachmentIds, replyAll); @@ -108,7 +109,7 @@ void main() { }); test('for message reply all', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); var enrollmentsApi = MockEnrollmentsApi(); var courseApi = MockCourseApi(); await setupTestLocator((locator) { @@ -136,7 +137,7 @@ void main() { String body = 'This is a reply'; List attachmentIds = ['a', 'b', 'c']; bool replyAll = true; - Message message = conversation.messages[3]; + Message message = conversation.messages![3]; await interactor.createReply(conversation, message, body, attachmentIds, replyAll); @@ -146,7 +147,7 @@ void main() { }); test('for message reply all filters out non-observed students', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); var enrollmentsApi = MockEnrollmentsApi(); var courseApi = MockCourseApi(); await setupTestLocator((locator) { @@ -177,7 +178,7 @@ void main() { String body = 'This is a reply'; List attachmentIds = ['a', 'b', 'c']; bool replyAll = true; - Message message = conversation.messages[4]; + Message message = conversation.messages![4]; await interactor.createReply(conversation, message, body, attachmentIds, replyAll); @@ -188,7 +189,7 @@ void main() { }); test('for self-authored message reply', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); }); @@ -199,7 +200,7 @@ void main() { String body = 'This is a reply'; List attachmentIds = ['a', 'b', 'c']; bool replyAll = false; - Message message = conversation.messages[2]; + Message message = conversation.messages![2]; interactor.createReply(conversation, message, body, attachmentIds, replyAll); @@ -209,7 +210,7 @@ void main() { }); test('for self-authored conversation reply', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); }); @@ -235,7 +236,7 @@ void main() { }); test('for monologue message reply', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); }); @@ -253,7 +254,7 @@ void main() { String body = 'This is a reply'; List attachmentIds = ['a', 'b', 'c']; bool replyAll = false; - Message message = conversation.messages[0]; + Message message = conversation.messages![0]; interactor.createReply(conversation, message, body, attachmentIds, replyAll); @@ -263,7 +264,7 @@ void main() { }); test('for monologue conversation reply', () async { - var api = _MockInboxApi(); + var api = MockInboxApi(); await setupTestLocator((locator) { locator.registerLazySingleton(() => api); }); @@ -337,5 +338,3 @@ Recipient _makeRecipient(String id, String type) { '123': BuiltList([type]) })); } - -class _MockInboxApi extends Mock implements InboxApi {} diff --git a/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_screen_test.dart b/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_screen_test.dart index a7d7366c3f..5fe8338b01 100644 --- a/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_screen_test.dart +++ b/apps/flutter_parent/test/screens/inbox/reply/conversation_reply_screen_test.dart @@ -42,6 +42,7 @@ import '../../../utils/accessibility_utils.dart'; import '../../../utils/finders.dart'; import '../../../utils/network_image_response.dart'; import '../../../utils/test_app.dart'; +import '../../../utils/test_helpers/mock_helpers.mocks.dart'; import '../create_conversation/create_conversation_screen_test.dart'; void main() { @@ -82,12 +83,12 @@ void main() { await _setupInteractor(); final conversation = _makeConversation(); - final message = conversation.messages[1]; + final message = conversation.messages![1]; await tester.pumpWidget(TestApp(ConversationReplyScreen(conversation, message, false))); await tester.pumpAndSettle(); - expect(find.descendant(of: find.byType(MessageWidget), matching: find.richText(message.body)), findsOneWidget); + expect(find.descendant(of: find.byType(MessageWidget), matching: find.richText(message.body!)), findsOneWidget); }); testWidgetsWithAccessibilityChecks('tapping attachment on message being replied to shows viewer', (tester) async { @@ -131,9 +132,9 @@ void main() { await tester.pumpWidget(TestApp(ConversationReplyScreen(conversation, null, false))); await tester.pumpAndSettle(); - final expectedMessage = conversation.messages[0]; + final expectedMessage = conversation.messages![0]; expect( - find.descendant(of: find.byType(MessageWidget), matching: find.richText(expectedMessage.body)), findsOneWidget); + find.descendant(of: find.byType(MessageWidget), matching: find.richText(expectedMessage.body!)), findsOneWidget); }); testWidgetsWithAccessibilityChecks('sending disabled when no message is present', (tester) async { @@ -179,7 +180,7 @@ void main() { testWidgetsWithAccessibilityChecks('sending calls interactor with correct parameters', (tester) async { final interactor = await _setupInteractor(); final conversation = _makeConversation(); - final message = conversation.messages[0]; + final message = conversation.messages![0]; final replyAll = true; final text = 'some text here'; @@ -505,6 +506,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(CreateConversationScreen.attachmentKey)); await tester.pump(); + await tester.pumpAndSettle(); // Assert attachment widget is displayed var attachmentWidget = find.byType(AttachmentWidget); @@ -512,6 +514,7 @@ void main() { await tester.longPress(attachmentWidget); await tester.pump(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); expect(find.text('file.txt'), findsOneWidget); }); @@ -528,6 +531,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(CreateConversationScreen.attachmentKey)); await tester.pump(); + await tester.pumpAndSettle(); // Assert attachment widget is displayed var attachmentWidget = find.byType(AttachmentWidget); @@ -535,6 +539,7 @@ void main() { await tester.longPress(attachmentWidget); await tester.pump(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); expect(find.text('file.txt'), findsOneWidget); }); @@ -553,6 +558,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(CreateConversationScreen.attachmentKey)); await tester.pump(); + await tester.pumpAndSettle(); // Assert attachment widget is displayed var attachmentWidget = find.byType(AttachmentWidget); @@ -560,6 +566,7 @@ void main() { await tester.longPress(attachmentWidget); await tester.pump(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); expect(find.text('upload.txt'), findsNWidgets(2)); // 2 widgets: one is the tooltip and one is the regular label }); @@ -633,11 +640,11 @@ void main() { } /// Load up a temp page with a button to navigate to our screen, that way the back button exists in the app bar -Future _pumpTestableWidgetWithBackButton(tester, Widget widget, {MockNavigatorObserver observer}) async { +Future _pumpTestableWidgetWithBackButton(tester, Widget widget, {MockNavigatorObserver? observer}) async { if (observer == null) observer = MockNavigatorObserver(); final app = TestApp( Builder( - builder: (context) => FlatButton( + builder: (context) => TextButton( child: Semantics(label: 'test', child: const SizedBox()), onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => widget)), ), @@ -647,15 +654,13 @@ Future _pumpTestableWidgetWithBackButton(tester, Widget widget, {MockNavig await tester.pumpWidget(app); await tester.pumpAndSettle(); - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); - if (observer != null) { - verify(observer.didPush(any, any)).called(2); // Twice, first for the initial page, then for the navigator route - } + verify(observer.didPush(any, any)).called(2); // Twice, first for the initial page, then for the navigator route } -Future<_MockInteractor> _setupInteractor() async { - final interactor = _MockInteractor(); +Future _setupInteractor() async { + final interactor = MockConversationReplyInteractor(); await setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -694,8 +699,6 @@ Conversation _makeConversation() { ])); } -class _MockInteractor extends Mock implements ConversationReplyInteractor {} - class _MockAttachmentHandler extends AttachmentHandler { _MockAttachmentHandler() : super(null); diff --git a/apps/flutter_parent/test/screens/login/domain_search_interactor_test.dart b/apps/flutter_parent/test/screens/login/domain_search_interactor_test.dart index 92608a3f93..bc773beb81 100644 --- a/apps/flutter_parent/test/screens/login/domain_search_interactor_test.dart +++ b/apps/flutter_parent/test/screens/login/domain_search_interactor_test.dart @@ -20,6 +20,7 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final launcher = MockUrlLauncher(); diff --git a/apps/flutter_parent/test/screens/login/domain_search_screen_test.dart b/apps/flutter_parent/test/screens/login/domain_search_screen_test.dart index 390aa206ff..6d23483109 100644 --- a/apps/flutter_parent/test/screens/login/domain_search_screen_test.dart +++ b/apps/flutter_parent/test/screens/login/domain_search_screen_test.dart @@ -30,12 +30,13 @@ import 'package:mockito/mockito.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { AppLocalizations l10n = AppLocalizations(); final analytics = MockAnalytics(); final webInteractor = MockWebLoginInteractor(); - final interactor = _MockInteractor(); + final interactor = MockDomainSearchInteractor(); setUp(() async { reset(analytics); @@ -213,12 +214,12 @@ void main() { await tester.tap(find.byType(TextField)); await tester.enterText(find.byType(TextField), 'testing123'); await tester.pumpAndSettle(); // Add in debounce time - expect(tester.widget(find.byType(TextField)).controller.text, 'testing123'); + expect(tester.widget(find.byType(TextField)).controller?.text, 'testing123'); expect(find.byKey(Key('clear-query')), findsOneWidget); await tester.tap(find.byKey(Key('clear-query'))); await tester.pump(); - expect(tester.widget(find.byType(TextField)).controller.text, ''); + expect(tester.widget(find.byType(TextField)).controller?.text, ''); // Wait for debounce to finish so test doesn't fail await tester.pump(Duration(milliseconds: 500)); @@ -302,12 +303,12 @@ void main() { // Get text selection for 'Canvas Support' span var targetText = l10n.canvasGuides; var bodyWidget = tester.widget(find.byKey(DomainSearchScreen.helpDialogBodyKey)); - var bodyText = bodyWidget.textSpan.toPlainText(); + var bodyText = bodyWidget.textSpan?.toPlainText() ?? ''; var index = bodyText.indexOf(targetText); var selection = TextSelection(baseOffset: index, extentOffset: index + targetText.length); // Get clickable area - RenderParagraph box = DomainSearchScreen.helpDialogBodyKey.currentContext.findRenderObject(); + RenderParagraph box = DomainSearchScreen.helpDialogBodyKey.currentContext?.findRenderObject() as RenderParagraph; var bodyOffset = box.localToGlobal(Offset.zero); var textOffset = box.getBoxesForSelection(selection)[0].toRect().center; @@ -327,12 +328,12 @@ void main() { // Get text selection for 'Canvas Support' span var targetText = l10n.canvasSupport; var bodyWidget = tester.widget(find.byKey(DomainSearchScreen.helpDialogBodyKey)); - var bodyText = bodyWidget.textSpan.toPlainText(); + var bodyText = bodyWidget.textSpan?.toPlainText() ?? ''; var index = bodyText.indexOf(targetText); var selection = TextSelection(baseOffset: index, extentOffset: index + targetText.length); // Get clickable area - RenderParagraph box = DomainSearchScreen.helpDialogBodyKey.currentContext.findRenderObject(); + RenderParagraph box = DomainSearchScreen.helpDialogBodyKey.currentContext?.findRenderObject() as RenderParagraph; var bodyOffset = box.localToGlobal(Offset.zero); var textOffset = box.getBoxesForSelection(selection)[0].toRect().center; @@ -484,5 +485,3 @@ void main() { expect(webLogin.domain, 'mobileqa.beta.instructure.com'); }); } - -class _MockInteractor extends Mock implements DomainSearchInteractor {} diff --git a/apps/flutter_parent/test/screens/login/login_landing_screen_test.dart b/apps/flutter_parent/test/screens/login/login_landing_screen_test.dart index fe10f63f69..2d0625c7c6 100644 --- a/apps/flutter_parent/test/screens/login/login_landing_screen_test.dart +++ b/apps/flutter_parent/test/screens/login/login_landing_screen_test.dart @@ -44,11 +44,12 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() async { - final analytics = _MockAnalytics(); - final interactor = _MockInteractor(); - final authApi = _MockAuthApi(); + final analytics = MockAnalytics(); + final interactor = MockDashboardInteractor(); + final authApi = MockAuthApi(); final pairingInteractor = MockPairingInteractor(); final login = Login((b) => b @@ -65,7 +66,7 @@ void main() async { locator.registerLazySingleton(() => pairingInteractor); locator.registerFactory(() => interactor); locator.registerFactory(() => SplashScreenInteractor()); - locator.registerFactory(() => _MockDomainSearchInteractor()); + locator.registerFactory(() => MockDomainSearchInteractor()); }); setUp(() async { @@ -75,6 +76,7 @@ void main() async { final mockRemoteConfig = setupMockRemoteConfig( valueSettings: {'qr_login_enabled_parent': 'true', 'qr_account_creation_enabled': 'true'}); await setupPlatformChannels(config: PlatformConfig(initRemoteConfig: mockRemoteConfig)); + await ApiPrefs.init(); }); tearDown(() { @@ -85,21 +87,20 @@ void main() async { Offset center = tester.getCenter(find.byType(TwoFingerDoubleTapGestureDetector)); // Perform first two-finger tap - TestGesture pointer1 = await tester.startGesture(center.translate(-64, 0)); - TestGesture pointer2 = await tester.startGesture(center.translate(64, 0)); + TestGesture pointer1 = await tester.startGesture(center.translate(-64, 64)); + TestGesture pointer2 = await tester.startGesture(center.translate(64, 64)); await pointer1.up(); await pointer2.up(); // Perform second two-finger tap await tester.pump(Duration(milliseconds: 100)); - pointer1 = await tester.startGesture(center.translate(-64, 0)); - pointer2 = await tester.startGesture(center.translate(64, 0)); + pointer1 = await tester.startGesture(center.translate(-64, 64)); + pointer2 = await tester.startGesture(center.translate(64, 64)); await pointer1.up(); await pointer2.up(); await tester.pump(); } - // TODO Fix test testWidgetsWithAccessibilityChecks('Opens domain search screen', (tester) async { await tester.pumpWidget(TestApp(LoginLandingScreen())); await tester.pumpAndSettle(); @@ -109,10 +110,7 @@ void main() async { await tester.pumpAndSettle(); expect(find.byType(DomainSearchScreen), findsOneWidget); - - // TODO: Remove this back press once DomainSearchScreen is passing accessibility checks - await tester.pageBack(); - }, skip: true); + }); testWidgetsWithAccessibilityChecks('Displays Snicker Doodles drawer', (tester) async { await tester.pumpWidget(TestApp(LoginLandingScreen())); @@ -134,6 +132,7 @@ void main() async { }); testWidgetsWithAccessibilityChecks('Displays login list if there are previous logins', (tester) async { + List logins = [ Login((b) => b ..domain = 'domain1' @@ -143,10 +142,12 @@ void main() async { ..user = CanvasModelTestUtils.mockUser(name: 'user 2').toBuilder()), ]; + await tester.pumpWidget(TestApp(LoginLandingScreen())); await ApiPrefs.saveLogins(logins); await tester.pumpAndSettle(); + expect(find.text(AppLocalizations().previousLogins), findsOneWidget); expect(find.byKey(Key('previous-logins')), findsOneWidget); @@ -157,67 +158,75 @@ void main() async { expect(find.text(logins[1].domain), findsOneWidget); expect(find.byType(Avatar), findsNWidgets(2)); - expect(find.bySemanticsLabel(AppLocalizations().delete), findsNWidgets(2)); + expect(find.byIcon(Icons.clear), findsNWidgets(2)); + }); testWidgetsWithAccessibilityChecks('Displays Previous Login correctly for masquerade', (tester) async { - Login login = Login((b) => b - ..domain = 'domain1' - ..masqueradeDomain = 'masqueradeDomain' - ..user = CanvasModelTestUtils.mockUser(name: 'user 1').toBuilder() - ..masqueradeUser = CanvasModelTestUtils.mockUser(name: 'masqueradeUser').toBuilder()); + await tester.runAsync(() async { - await tester.pumpWidget(TestApp(LoginLandingScreen())); - await ApiPrefs.saveLogins([login]); - await tester.pumpAndSettle(); - - expect(find.text(AppLocalizations().previousLogins), findsOneWidget); - expect(find.byKey(Key('previous-logins')), findsOneWidget); - - expect(find.text(login.user.name), findsNothing); - expect(find.text(login.masqueradeUser.name), findsOneWidget); - - expect(find.text(login.domain), findsNothing); - expect(find.text(login.masqueradeDomain), findsOneWidget); - - expect(find.byType(Avatar), findsOneWidget); - expect(find.bySemanticsLabel(AppLocalizations().delete), findsOneWidget); - expect(find.byIcon(CanvasIconsSolid.masquerade), findsOneWidget); - }); - - testWidgetsWithAccessibilityChecks('Clearing previous login removes it from the list', (tester) async { - List logins = [ - Login((b) => b + Login login = Login((b) => b ..domain = 'domain1' - ..user = CanvasModelTestUtils.mockUser(name: 'user 1').toBuilder()), - Login((b) => b - ..domain = 'domain2' - ..user = CanvasModelTestUtils.mockUser(name: 'user 2').toBuilder()), - ]; + ..masqueradeDomain = 'masqueradeDomain' + ..user = CanvasModelTestUtils.mockUser(name: 'user 1').toBuilder() + ..masqueradeUser = CanvasModelTestUtils.mockUser(name: 'masqueradeUser').toBuilder()); - await tester.pumpWidget(TestApp(LoginLandingScreen())); - await ApiPrefs.saveLogins(logins); - await tester.pumpAndSettle(); + await tester.pumpWidget(TestApp(LoginLandingScreen())); + await ApiPrefs.saveLogins([login]); + await tester.pumpAndSettle(); - expect(find.byKey(Key('previous-logins')), findsOneWidget); - expect(find.text(logins[0].user.name), findsOneWidget); - expect(find.text(logins[1].user.name), findsOneWidget); + expect(find.text(AppLocalizations().previousLogins), findsOneWidget); + expect(find.byKey(Key('previous-logins')), findsOneWidget); - // Remove second login - await tester.tap(find.bySemanticsLabel(AppLocalizations().delete).last); - await tester.pumpAndSettle(); + expect(find.text(login.user.name), findsNothing); + expect(find.text(login.masqueradeUser!.name), findsOneWidget); - expect(find.byKey(Key('previous-logins')), findsOneWidget); - expect(find.text(logins[0].user.name), findsOneWidget); - expect(find.text(logins[1].user.name), findsNothing); + expect(find.text(login.domain), findsNothing); + expect(find.text(login.masqueradeDomain!), findsOneWidget); - // Remove first login - await tester.tap(find.bySemanticsLabel(AppLocalizations().delete)); - await tester.pumpAndSettle(); + expect(find.byType(Avatar), findsOneWidget); + expect(find.byIcon(Icons.clear), findsOneWidget); + expect(find.byIcon(CanvasIconsSolid.masquerade), findsOneWidget); + }); + }); - expect(find.byKey(Key('previous-logins')), findsNothing); - expect(find.text(logins[0].user.name), findsNothing); - expect(find.text(logins[1].user.name), findsNothing); + testWidgetsWithAccessibilityChecks('Clearing previous login removes it from the list', (tester) async { + await tester.runAsync(() async { + + List logins = [ + Login((b) => b + ..domain = 'domain1' + ..user = CanvasModelTestUtils.mockUser(name: 'user 1').toBuilder()), + Login((b) => b + ..domain = 'domain2' + ..user = CanvasModelTestUtils.mockUser(name: 'user 2').toBuilder()), + ]; + + await tester.pumpWidget(TestApp(LoginLandingScreen())); + await ApiPrefs.saveLogins(logins); + await tester.pumpAndSettle(); + + expect(find.byKey(Key('previous-logins')), findsOneWidget); + expect(find.text(logins[0].user.name), findsOneWidget); + expect(find.text(logins[1].user.name), findsOneWidget); + + // Remove second login + await tester.tap(find.byIcon(Icons.clear).last); + await tester.pumpAndSettle(); + + expect(find.byKey(Key('previous-logins')), findsOneWidget); + expect(find.text(logins[0].user.name), findsOneWidget); + expect(find.text(logins[1].user.name), findsNothing); + + // Remove first login + await tester.tap(find.byIcon(Icons.clear)); + await tester.pumpAndSettle(); + + expect(find.byKey(Key('previous-logins')), findsNothing); + expect(find.text(logins[0].user.name), findsNothing); + expect(find.text(logins[1].user.name), findsNothing); + + }); }); testWidgetsWithAccessibilityChecks('Tapping a login sets the current login and loads splash screen', (tester) async { @@ -237,7 +246,7 @@ void main() async { expect(find.byType(SplashScreen), findsOneWidget); expect(ApiPrefs.getCurrentLogin(), logins[0]); - ApiPrefs.clean(); + await ApiPrefs.clean(); }); testWidgetsWithAccessibilityChecks('Uses two-finger double-tap to cycle login flows', (tester) async { @@ -263,7 +272,6 @@ void main() async { await tester.pumpAndSettle(); // Wait for SnackBar to finish displaying }); - // TODO Fix test testWidgetsWithAccessibilityChecks('Passes selected LoginFlow to DomainSearchScreen', (tester) async { await tester.pumpWidget(TestApp(LoginLandingScreen())); await tester.pumpAndSettle(); @@ -282,9 +290,7 @@ void main() async { DomainSearchScreen domainSearch = tester.widget(find.byType(DomainSearchScreen)); expect(domainSearch.loginFlow, LoginFlow.skipMobileVerify); - // TODO: Remove this back press once DomainSearchScreen is passing accessibility checks - await tester.pageBack(); - }, skip: true); + }); testWidgetsWithAccessibilityChecks('Tapping QR login shows QR Login picker', (tester) async { await tester.pumpWidget(TestApp(LoginLandingScreen())); @@ -335,11 +341,3 @@ void main() async { expect(find.text(AppLocalizations().qrCode), findsNothing); }); } - -class _MockAnalytics extends Mock implements Analytics {} - -class _MockInteractor extends Mock implements DashboardInteractor {} - -class _MockAuthApi extends Mock implements AuthApi {} - -class _MockDomainSearchInteractor extends Mock implements DomainSearchInteractor {} diff --git a/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_interactor_test.dart b/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_interactor_test.dart index 862ed147e7..4a777b3b19 100644 --- a/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_interactor_test.dart +++ b/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_interactor_test.dart @@ -24,10 +24,11 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final interactor = QRLoginTutorialScreenInteractor(); - final mockScanner = MockBarcodeScanner(); + final mockScanner = MockBarcodeScanVeneer(); setupTestLocator((locator) { locator.registerLazySingleton(() => mockScanner); diff --git a/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_test.dart b/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_test.dart index b27f374ede..5e8608fa10 100644 --- a/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_test.dart +++ b/apps/flutter_parent/test/screens/login/qr_login_tutorial_screen_test.dart @@ -26,12 +26,13 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final barcodeResultUrl = 'https://${QRUtils.QR_HOST}/canvas/login?${QRUtils.QR_AUTH_CODE}=1234' '&${QRUtils.QR_DOMAIN}=mobiledev.instructure.com'; - final interactor = _MockInteractor(); - final mockNav = _MockNav(); + final interactor = MockQRLoginTutorialScreenInteractor(); + final mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); @@ -49,8 +50,8 @@ void main() { await tester.runAsync(() async { Element element = tester.element(find.byType(FractionallySizedBox)); - final FractionallySizedBox widget = element.widget; - final Image image = widget.child; + final FractionallySizedBox widget = element.widget as FractionallySizedBox; + final Image image = widget.child as Image; final ImageProvider imageProvider = image.image; await precacheImage(imageProvider, element); await tester.pumpAndSettle(); @@ -116,8 +117,4 @@ void main() { await tester.pumpAndSettle(); expect(find.text(AppLocalizations().loginWithQRCodeError), findsOneWidget); }); -} - -class _MockInteractor extends Mock implements QRLoginTutorialScreenInteractor {} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/login/web_login_interactor_test.dart b/apps/flutter_parent/test/screens/login/web_login_interactor_test.dart index 2cfa2ab133..352d6079af 100644 --- a/apps/flutter_parent/test/screens/login/web_login_interactor_test.dart +++ b/apps/flutter_parent/test/screens/login/web_login_interactor_test.dart @@ -22,9 +22,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - final api = _MockAuthApi(); + final api = MockAuthApi(); setupTestLocator((locator) { locator.registerLazySingleton(() => api); @@ -62,5 +63,3 @@ void main() { expect(ApiPrefs.getUser(), user); }); } - -class _MockAuthApi extends Mock implements AuthApi {} diff --git a/apps/flutter_parent/test/screens/login/web_login_screen_test.dart b/apps/flutter_parent/test/screens/login/web_login_screen_test.dart index 0ab60888c0..96328bacfa 100644 --- a/apps/flutter_parent/test/screens/login/web_login_screen_test.dart +++ b/apps/flutter_parent/test/screens/login/web_login_screen_test.dart @@ -27,6 +27,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final interactor = MockWebLoginInteractor(); diff --git a/apps/flutter_parent/test/screens/manage_students/manage_students_interactor_test.dart b/apps/flutter_parent/test/screens/manage_students/manage_students_interactor_test.dart index 766af11a92..b0f51297c7 100644 --- a/apps/flutter_parent/test/screens/manage_students/manage_students_interactor_test.dart +++ b/apps/flutter_parent/test/screens/manage_students/manage_students_interactor_test.dart @@ -24,6 +24,7 @@ import 'package:test/test.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final api = MockEnrollmentsApi(); @@ -100,7 +101,7 @@ User _mockStudent(String name) => User((b) => b ..sortableName = name ..build()); -Enrollment _mockEnrollment(UserBuilder observedUser) => Enrollment((b) => b +Enrollment _mockEnrollment(UserBuilder? observedUser) => Enrollment((b) => b ..enrollmentState = '' ..observedUser = observedUser ..build()); diff --git a/apps/flutter_parent/test/screens/manage_students/manage_students_screen_test.dart b/apps/flutter_parent/test/screens/manage_students/manage_students_screen_test.dart index 307948f0bd..970beac970 100644 --- a/apps/flutter_parent/test/screens/manage_students/manage_students_screen_test.dart +++ b/apps/flutter_parent/test/screens/manage_students/manage_students_screen_test.dart @@ -38,22 +38,23 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/network_image_response.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { mockNetworkImageResponse(); - final analytics = _MockAnalytics(); + final analytics = MockAnalytics(); final MockPairingUtil pairingUtil = MockPairingUtil(); final MockUserColorsDb userColorsDb = MockUserColorsDb(); - _setupLocator([_MockManageStudentsInteractor interactor]) async { + _setupLocator([MockManageStudentsInteractor? interactor]) async { final locator = GetIt.instance; await locator.reset(); - var thresholdInteractor = _MockAlertThresholdsInteractor(); + var thresholdInteractor = MockAlertThresholdsInteractor(); when(thresholdInteractor.getAlertThresholdsForStudent(any)).thenAnswer((_) => Future.value([])); locator.registerFactory(() => thresholdInteractor); - locator.registerFactory(() => interactor ?? _MockManageStudentsInteractor()); + locator.registerFactory(() => interactor ?? MockManageStudentsInteractor()); locator.registerFactory(() => QuickNav()); locator.registerLazySingleton(() => analytics); locator.registerLazySingleton(() => pairingUtil); @@ -66,9 +67,9 @@ void main() { reset(userColorsDb); }); - void _clickFAB(WidgetTester tester) async { - await tester.tap(find.byType(FloatingActionButton)); - await tester.pumpAndSettle(); + Future _clickFAB(WidgetTester? tester) async { + await tester?.tap(find.byType(FloatingActionButton)); + await tester?.pumpAndSettle(); } group('Refresh', () { @@ -77,7 +78,7 @@ void main() { var postRefreshStudent = [CanvasModelTestUtils.mockUser(shortName: 'Sally')]; // Mock the behavior of the interactor to return a student - final interactor = _MockManageStudentsInteractor(); + final interactor = MockManageStudentsInteractor(); when(interactor.getStudents(forceRefresh: anyNamed('forceRefresh'))) .thenAnswer((_) => Future.value(postRefreshStudent)); _setupLocator(interactor); @@ -99,8 +100,8 @@ void main() { }); testWidgetsWithAccessibilityChecks('Error on pull to refresh', (tester) async { - var interactor = _MockManageStudentsInteractor(); - Completer completer = Completer>(); + var interactor = MockManageStudentsInteractor(); + Completer?> completer = Completer?>(); when(interactor.getStudents(forceRefresh: anyNamed('forceRefresh'))).thenAnswer((_) => completer.future); _setupLocator(interactor); @@ -127,8 +128,8 @@ void main() { var observedStudents = [CanvasModelTestUtils.mockUser(shortName: 'Billy')]; // Mock interactor to return an error when retrieving student list - var interactor = _MockManageStudentsInteractor(); - Completer completer = Completer>(); + var interactor = MockManageStudentsInteractor(); + Completer?> completer = Completer?>(); when(interactor.getStudents(forceRefresh: anyNamed('forceRefresh'))).thenAnswer((_) => completer.future); _setupLocator(interactor); @@ -320,6 +321,7 @@ void main() { // Pump and settle the page transition animation await tester.pump(); await tester.pump(); + await tester.pumpAndSettle(); // Find the thresholds screen expect(find.byType(AlertThresholdsScreen), findsOneWidget); @@ -328,7 +330,7 @@ void main() { group('Add Student', () { testWidgetsWithAccessibilityChecks('Displays FAB for pairing', (tester) async { - var interactor = _MockManageStudentsInteractor(); + var interactor = MockManageStudentsInteractor(); _setupLocator(interactor); await tester.pumpWidget(TestApp( @@ -341,7 +343,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Tapping FAB calls PairingUtil', (tester) async { - var interactor = _MockManageStudentsInteractor(); + var interactor = MockManageStudentsInteractor(); _setupLocator(interactor); var observedStudents = [CanvasModelTestUtils.mockUser(name: 'Billy')]; @@ -362,7 +364,7 @@ void main() { var observedStudent = [CanvasModelTestUtils.mockUser(shortName: 'Billy')]; // Mock return value for success when pairing a student - final interactor = _MockManageStudentsInteractor(); + final interactor = MockManageStudentsInteractor(); when(pairingUtil.pairNewStudent(any, any)).thenAnswer((inv) => inv.positionalArguments[1]()); // Mock retrieving students, also add an extra student to the list @@ -398,7 +400,7 @@ void main() { var observedStudent = [CanvasModelTestUtils.mockUser(shortName: 'Billy', id: "1771")]; // Mock return value for success when pairing a student - final interactor = _MockManageStudentsInteractor(); + final interactor = MockManageStudentsInteractor(); when(pairingUtil.pairNewStudent(any, any)).thenAnswer((inv) => inv.positionalArguments[1]()); // Mock retrieving students, also add an extra student to the list @@ -409,7 +411,7 @@ void main() { _setupLocator(interactor); - final observer = _MockNavigatorObserver(); + final observer = MockNavigatorObserver(); // Setup page await _pumpTestableWidgetWithBackButton(tester, ManageStudentsScreen(observedStudent), observer); @@ -445,11 +447,11 @@ void main() { } /// Load up a temp page with a button to navigate to our screen, that way the back button exists in the app bar -Future _pumpTestableWidgetWithBackButton(tester, Widget widget, _MockNavigatorObserver observer) async { - var mockObserver = _MockNavigatorObserver(); +Future _pumpTestableWidgetWithBackButton(tester, Widget widget, MockNavigatorObserver observer) async { + var mockObserver = MockNavigatorObserver(); final app = TestApp( Builder( - builder: (context) => FlatButton( + builder: (context) => TextButton( child: Semantics(label: 'test', child: const SizedBox()), onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => widget)), ), @@ -459,15 +461,7 @@ Future _pumpTestableWidgetWithBackButton(tester, Widget widget, _MockNavig await tester.pumpWidget(app); await tester.pumpAndSettle(); - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); verify(mockObserver.didPush(any, any)).called(2); // Twice, first for the initial page, then for the navigator route -} - -class _MockManageStudentsInteractor extends Mock implements ManageStudentsInteractor {} - -class _MockAlertThresholdsInteractor extends Mock implements AlertThresholdsInteractor {} - -class _MockAnalytics extends Mock implements Analytics {} - -class _MockNavigatorObserver extends Mock implements NavigatorObserver {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/manage_students/student_color_picker_dialog_test.dart b/apps/flutter_parent/test/screens/manage_students/student_color_picker_dialog_test.dart index 367fdd664f..5530ab2f3b 100644 --- a/apps/flutter_parent/test/screens/manage_students/student_color_picker_dialog_test.dart +++ b/apps/flutter_parent/test/screens/manage_students/student_color_picker_dialog_test.dart @@ -19,17 +19,19 @@ import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/user_colors.dart'; import 'package:flutter_parent/screens/manage_students/student_color_picker_dialog.dart'; import 'package:flutter_parent/screens/manage_students/student_color_picker_interactor.dart'; +import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/design/student_color_set.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import '../pairing/pairing_util_test.dart'; void main() { AppLocalizations l10n = AppLocalizations(); - _MockStudentColorPickerInteractor interactor = _MockStudentColorPickerInteractor(); + MockStudentColorPickerInteractor interactor = MockStudentColorPickerInteractor(); setupTestLocator((locator) { locator.registerLazySingleton(() => interactor); @@ -62,7 +64,7 @@ void main() { await tester.pumpAndSettle(); // 'Plum' color should be selected - var predicate = (Widget w) => w is Semantics && w.properties.label == l10n.colorPlum && w.properties.selected; + var predicate = (Widget w) => w is Semantics && w.properties.label == l10n.colorPlum && w.properties.selected!; expect(find.byWidgetPredicate(predicate), findsOneWidget); }); @@ -104,7 +106,8 @@ void main() { await tester.pumpAndSettle(); // Tap 'Ok' and wait for the result - await tester.tap(find.bySemanticsLabel(l10n.ok)); + await tester.tap(find.text(l10n.ok)); + await tester.pumpAndSettle(); var result = await resultFuture; // Should have returned true @@ -172,6 +175,4 @@ void main() { // Should show error message expect(find.text(l10n.errorSavingColor), findsOneWidget); }); -} - -class _MockStudentColorPickerInteractor extends Mock implements StudentColorPickerInteractor {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/manage_students/student_color_picker_interactor_test.dart b/apps/flutter_parent/test/screens/manage_students/student_color_picker_interactor_test.dart index 173dbf46ce..328dc13421 100644 --- a/apps/flutter_parent/test/screens/manage_students/student_color_picker_interactor_test.dart +++ b/apps/flutter_parent/test/screens/manage_students/student_color_picker_interactor_test.dart @@ -26,7 +26,7 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; -import '../dashboard/dashboard_interactor_test.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() async { Login login = Login((b) => b diff --git a/apps/flutter_parent/test/screens/masquerade/masquerade_screen_interactor_test.dart b/apps/flutter_parent/test/screens/masquerade/masquerade_screen_interactor_test.dart index 04666b1501..11dd6545a5 100644 --- a/apps/flutter_parent/test/screens/masquerade/masquerade_screen_interactor_test.dart +++ b/apps/flutter_parent/test/screens/masquerade/masquerade_screen_interactor_test.dart @@ -22,9 +22,10 @@ import 'package:test/test.dart'; import '../../utils/canvas_model_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - _MockUserApi userApi = _MockUserApi(); + MockUserApi userApi = MockUserApi(); setupTestLocator((locator) { locator.registerLazySingleton(() => userApi); @@ -39,7 +40,7 @@ void main() { String expectedDomain = 'domain'; ApiPrefs.switchLogins(Login((b) => b..domain = expectedDomain)); - String actualDomain = MasqueradeScreenInteractor().getDomain(); + String? actualDomain = MasqueradeScreenInteractor().getDomain(); expect(actualDomain, expectedDomain); }); @@ -141,7 +142,7 @@ void main() { }); test('sanitizeDomain returns an empty string for null input', () { - String input = null; + String? input = null; String expected = ''; String actual = MasqueradeScreenInteractor().sanitizeDomain(input); @@ -156,6 +157,4 @@ void main() { expect(actual, expected); }); }); -} - -class _MockUserApi extends Mock implements UserApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/masquerade/masquerade_screen_test.dart b/apps/flutter_parent/test/screens/masquerade/masquerade_screen_test.dart index 14319acda6..babd223e8b 100644 --- a/apps/flutter_parent/test/screens/masquerade/masquerade_screen_test.dart +++ b/apps/flutter_parent/test/screens/masquerade/masquerade_screen_test.dart @@ -24,9 +24,10 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - _MockInteractor interactor = _MockInteractor(); + MockMasqueradeScreenInteractor interactor = MockMasqueradeScreenInteractor(); String siteAdminDomain = 'https://siteadmin.instructure.com'; String normalDomain = 'https://example.instructure.com'; @@ -101,7 +102,7 @@ void main() { TextField input = tester.widget(find.byKey(domainKey)); expect(input.enabled, isFalse); - expect(input.controller.text, normalDomain); + expect(input.controller!.text, normalDomain); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, ); @@ -115,7 +116,7 @@ void main() { TextField input = tester.widget(find.byKey(domainKey)); expect(input.enabled, isTrue); - expect(input.controller.text, isEmpty); + expect(input.controller!.text, isEmpty); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, ); @@ -128,12 +129,12 @@ void main() { await tester.pump(); // Tap the 'Act As User' button - domain input should be empty - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Error message should show TextField input = tester.widget(find.byKey(domainKey)); - expect(input.decoration.errorText, l10n.domainInputError); + expect(input.decoration?.errorText, l10n.domainInputError); expect(find.text(l10n.domainInputError), findsOneWidget); // Input a valid domain @@ -142,16 +143,16 @@ void main() { // Entering text should have cleared the error input = tester.widget(find.byKey(domainKey)); - expect(input.decoration.errorText, isNull); + expect(input.decoration?.errorText, isNull); expect(find.text(l10n.domainInputError), findsNothing); // Tap the 'Act As User' button again - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // The error should not be displayed input = tester.widget(find.byKey(domainKey)); - expect(input.decoration.errorText, isNull); + expect(input.decoration?.errorText, isNull); expect(find.text(l10n.domainInputError), findsNothing); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, @@ -165,12 +166,12 @@ void main() { await tester.pump(); // Tap the 'Act As User' button - user id input should be empty - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Error message should show TextField input = tester.widget(find.byKey(userIdKey)); - expect(input.decoration.errorText, l10n.userIdInputError); + expect(input.decoration?.errorText, l10n.userIdInputError); expect(find.text(l10n.userIdInputError), findsOneWidget); // Input a valid user id @@ -179,16 +180,16 @@ void main() { // Entering text should have cleared the error input = tester.widget(find.byKey(userIdKey)); - expect(input.decoration.errorText, isNull); + expect(input.decoration?.errorText, isNull); expect(find.text(l10n.userIdInputError), findsNothing); // Tap the 'Act As User' button again - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // The error should not be displayed input = tester.widget(find.byKey(userIdKey)); - expect(input.decoration.errorText, isNull); + expect(input.decoration?.errorText, isNull); expect(find.text(l10n.userIdInputError), findsNothing); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, @@ -205,10 +206,10 @@ void main() { // Enter a user id and press the button await tester.enterText(find.byKey(userIdKey), '123'); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); - expect(find.byType(RaisedButton), findsNothing); + expect(find.byType(ElevatedButton), findsNothing); expect(find.byType(CircularProgressIndicator), findsOneWidget); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, @@ -225,11 +226,11 @@ void main() { // Enter a user id and press the button await tester.enterText(find.byKey(userIdKey), '123'); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Should show loading state - expect(find.byType(RaisedButton), findsNothing); + expect(find.byType(ElevatedButton), findsNothing); expect(find.byType(CircularProgressIndicator), findsOneWidget); completer.complete(false); @@ -237,7 +238,7 @@ void main() { // Should show error message and no loading state expect(find.text(l10n.actAsUserError), findsOneWidget); - expect(find.byType(RaisedButton), findsOneWidget); + expect(find.byType(ElevatedButton), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsNothing); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, @@ -255,18 +256,16 @@ void main() { // Enter data and tap the button await tester.enterText(find.byKey(domainKey), normalDomain); await tester.enterText(find.byKey(userIdKey), '123'); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Respawn should have created a new screen with empty fields TextField domainInput = tester.widget(find.byKey(domainKey)); - expect(domainInput.controller.text, isEmpty); + expect(domainInput.controller?.text, isEmpty); TextField userIdInput = tester.widget(find.byKey(userIdKey)); - expect(userIdInput.controller.text, isEmpty); + expect(userIdInput.controller?.text, isEmpty); }, a11yExclusions: {A11yExclusion.multipleNodesWithSameLabel}, ); } - -class _MockInteractor extends Mock implements MasqueradeScreenInteractor {} diff --git a/apps/flutter_parent/test/screens/not_a_parent_screen_test.dart b/apps/flutter_parent/test/screens/not_a_parent_screen_test.dart index 6bb6c2211d..cb468b7e83 100644 --- a/apps/flutter_parent/test/screens/not_a_parent_screen_test.dart +++ b/apps/flutter_parent/test/screens/not_a_parent_screen_test.dart @@ -30,6 +30,7 @@ import 'package:mockito/mockito.dart'; import '../utils/accessibility_utils.dart'; import '../utils/platform_config.dart'; import '../utils/test_app.dart'; +import '../utils/test_helpers/mock_helpers.mocks.dart'; void main() { AppLocalizations l10n = AppLocalizations(); @@ -46,13 +47,13 @@ void main() { }); testWidgetsWithAccessibilityChecks('Can return to login', (tester) async { - final mockNav = _MockQuickNav(); + final mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerLazySingleton(() => mockNav); - locator.registerLazySingleton(() => _MockReminderDb()); - locator.registerLazySingleton(() => _MockNotificationUtil()); - locator.registerLazySingleton(() => _MockCalendarFilterDb()); - locator.registerLazySingleton(() => _MockAuthApi()); + locator.registerLazySingleton(() => MockReminderDb()); + locator.registerLazySingleton(() => MockNotificationUtil()); + locator.registerLazySingleton(() => MockCalendarFilterDb()); + locator.registerLazySingleton(() => MockAuthApi()); }); await tester.pumpWidget(TestApp( @@ -83,7 +84,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Launches intent to open student app in play store', (tester) async { - var mockLauncher = _MockUrlLauncher(); + var mockLauncher = MockUrlLauncher(); setupTestLocator((locator) => locator.registerLazySingleton(() => mockLauncher)); await tester.pumpWidget(TestApp(NotAParentScreen())); @@ -102,7 +103,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Launches intent to open teacher app in play store', (tester) async { - var mockLauncher = _MockUrlLauncher(); + var mockLauncher = MockUrlLauncher(); setupTestLocator((locator) => locator.registerLazySingleton(() => mockLauncher)); await tester.pumpWidget(TestApp(NotAParentScreen())); @@ -119,16 +120,4 @@ void main() { var actualUrl = verify(mockLauncher.launch(captureAny)).captured[0]; expect(actualUrl, 'market://details?id=com.instructure.teacher'); }); -} - -class _MockUrlLauncher extends Mock implements UrlLauncher {} - -class _MockQuickNav extends Mock implements QuickNav {} - -class _MockReminderDb extends Mock implements ReminderDb {} - -class _MockNotificationUtil extends Mock implements NotificationUtil {} - -class _MockCalendarFilterDb extends Mock implements CalendarFilterDb {} - -class _MockAuthApi extends Mock implements AuthApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/pairing/pairing_code_dialog_test.dart b/apps/flutter_parent/test/screens/pairing/pairing_code_dialog_test.dart index 33e34e1410..9c2bd1e9a8 100644 --- a/apps/flutter_parent/test/screens/pairing/pairing_code_dialog_test.dart +++ b/apps/flutter_parent/test/screens/pairing/pairing_code_dialog_test.dart @@ -22,12 +22,13 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import 'pairing_util_test.dart'; void main() { AppLocalizations l10n = AppLocalizations(); - PairingInteractor interactor = MockPairingInteractor(); + MockPairingInteractor interactor = MockPairingInteractor(); setupTestLocator((locator) { locator.registerLazySingleton(() => interactor); @@ -55,7 +56,7 @@ void main() { expect(find.byType(TextFormField), findsOneWidget); // Buttons - var button = find.byType(FlatButton); + var button = find.byType(TextButton); expect(find.descendant(of: button, matching: find.text(l10n.cancel.toUpperCase())), findsOneWidget); expect(find.descendant(of: button, matching: find.text(l10n.ok.toUpperCase())), findsOneWidget); }); diff --git a/apps/flutter_parent/test/screens/pairing/pairing_interactor_test.dart b/apps/flutter_parent/test/screens/pairing/pairing_interactor_test.dart index 63926427b7..88882e0382 100644 --- a/apps/flutter_parent/test/screens/pairing/pairing_interactor_test.dart +++ b/apps/flutter_parent/test/screens/pairing/pairing_interactor_test.dart @@ -22,9 +22,10 @@ import 'package:mockito/mockito.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - MockBarcodeScanner scanner = MockBarcodeScanner(); + MockBarcodeScanVeneer scanner = MockBarcodeScanVeneer(); MockEnrollmentsApi enrollmentsApi = MockEnrollmentsApi(); setupTestLocator((locator) { diff --git a/apps/flutter_parent/test/screens/pairing/pairing_util_test.dart b/apps/flutter_parent/test/screens/pairing/pairing_util_test.dart index 3c3cd7411f..d6af326024 100644 --- a/apps/flutter_parent/test/screens/pairing/pairing_util_test.dart +++ b/apps/flutter_parent/test/screens/pairing/pairing_util_test.dart @@ -25,6 +25,7 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { AppLocalizations l10n = AppLocalizations(); @@ -62,7 +63,7 @@ void main() { ApiPrefs.setCameraCount(0); BuildContext context = tester.state(find.byType(DummyWidget)).context; - PairingUtil().pairNewStudent(context, () => null); + PairingUtil().pairNewStudent(context, () => {}); await tester.pumpAndSettle(); expect(find.text(l10n.pairingCode), findsOneWidget); @@ -77,15 +78,23 @@ void main() { PairingUtil().pairNewStudent(context, () => null); await tester.pumpAndSettle(); - await tester.tap(find.text(l10n.pairingCode)); - await tester.pumpAndSettle(); - - verify( + when( nav.showDialog( context: anyNamed('context'), barrierDismissible: anyNamed('barrierDismissible'), builder: anyNamed('builder'), ), + ).thenAnswer((_) async => true); + + await tester.tap(find.text(l10n.pairingCode)); + await tester.pumpAndSettle(); + + verify( + nav.showDialog( + context: anyNamed('context'), + barrierDismissible: anyNamed('barrierDismissible'), + builder: anyNamed('builder'), + ) ); }); diff --git a/apps/flutter_parent/test/screens/pairing/qr_pairing_screen_test.dart b/apps/flutter_parent/test/screens/pairing/qr_pairing_screen_test.dart index c9c0088d64..62a31947b8 100644 --- a/apps/flutter_parent/test/screens/pairing/qr_pairing_screen_test.dart +++ b/apps/flutter_parent/test/screens/pairing/qr_pairing_screen_test.dart @@ -33,14 +33,15 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import 'pairing_util_test.dart'; void main() { AppLocalizations l10n = AppLocalizations(); - PairingInteractor interactor = MockPairingInteractor(); - QuickNav nav = MockQuickNav(); - StudentAddedNotifier studentAddedNotifier = MockStudentAddedNotifier(); + MockPairingInteractor interactor = MockPairingInteractor(); + MockQuickNav nav = MockQuickNav(); + MockStudentAddedNotifier studentAddedNotifier = MockStudentAddedNotifier(); setupTestLocator((locator) { locator.registerLazySingleton(() => interactor); @@ -101,7 +102,7 @@ void main() { testWidgetsWithAccessibilityChecks('Navigates to splash screen on success if first route', (tester) async { when(interactor.pairWithStudent(any)).thenAnswer((_) async => true); - await tester.pumpWidget(TestApp(QRPairingScreen(pairingInfo: QRPairingScanResult.success('', '', '')))); + await tester.pumpWidget(TestApp(QRPairingScreen(pairingInfo: QRPairingScanResult.success('', '', '') as QRPairingInfo))); await tester.pump(); verify(nav.replaceRoute(any, PandaRouter.rootSplash())); @@ -267,7 +268,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('Initiates pairing on launch if pairing info is provided', (tester) async { - QRPairingInfo pairingInfo = QRPairingScanResult.success('123acb', '', ''); + QRPairingInfo pairingInfo = QRPairingScanResult.success('123acb', '', '') as QRPairingInfo; await tester.pumpWidget(TestApp(QRPairingScreen(pairingInfo: pairingInfo))); await tester.pump(); diff --git a/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart b/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart index 0c9756e102..ea11ba657e 100644 --- a/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart +++ b/apps/flutter_parent/test/screens/remote_config_params/remote_config_screen_test.dart @@ -23,11 +23,12 @@ import '../../utils/accessibility_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { testWidgetsWithAccessibilityChecks('Shows the correct list', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockRemoteConfigInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); Map remoteConfigs = { @@ -46,5 +47,3 @@ void main() { expect(find.text('fetched value'), findsOneWidget); }); } - -class _MockInteractor extends Mock implements RemoteConfigInteractor {} diff --git a/apps/flutter_parent/test/screens/help/legal_screen_test.dart b/apps/flutter_parent/test/screens/settings/legal_screen_test.dart similarity index 90% rename from apps/flutter_parent/test/screens/help/legal_screen_test.dart rename to apps/flutter_parent/test/screens/settings/legal_screen_test.dart index 812c1c54ad..6055ed0335 100644 --- a/apps/flutter_parent/test/screens/help/legal_screen_test.dart +++ b/apps/flutter_parent/test/screens/settings/legal_screen_test.dart @@ -13,7 +13,7 @@ // along with this program. If not, see . import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/router/panda_router.dart'; -import 'package:flutter_parent/screens/help/legal_screen.dart'; +import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -21,6 +21,7 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final l10n = AppLocalizations(); @@ -37,7 +38,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('tapping privacy policy launches url', (tester) async { - var mockLauncher = _MockUrlLauncher(); + var mockLauncher = MockUrlLauncher(); setupTestLocator((locator) => locator.registerLazySingleton(() => mockLauncher)); await tester.pumpWidget(TestApp(LegalScreen())); @@ -50,7 +51,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('tapping github launches url', (tester) async { - var mockLauncher = _MockUrlLauncher(); + var mockLauncher = MockUrlLauncher(); setupTestLocator((locator) => locator.registerLazySingleton(() => mockLauncher)); await tester.pumpWidget(TestApp(LegalScreen())); @@ -63,7 +64,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('tapping terms of use navigates to Terms of Use screen', (tester) async { - final nav = _MockNav(); + final nav = MockQuickNav(); setupTestLocator((locator) => locator.registerSingleton(nav)); await tester.pumpWidget(TestApp(LegalScreen())); @@ -74,8 +75,4 @@ void main() { verify(nav.pushRoute(any, argThat(matches(PandaRouter.termsOfUse())))); }); -} - -class _MockUrlLauncher extends Mock implements UrlLauncher {} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart b/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart index 312ae89be3..820183aa22 100644 --- a/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart +++ b/apps/flutter_parent/test/screens/settings/settings_interactor_test.dart @@ -13,8 +13,8 @@ // along with this program. If not, see . import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; +import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/remote_config/remote_config_screen.dart'; import 'package:flutter_parent/screens/settings/settings_interactor.dart'; import 'package:flutter_parent/screens/theme_viewer_screen.dart'; @@ -25,6 +25,7 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { test('Returns true for debug mode', () { @@ -33,12 +34,12 @@ void main() { }); test('routeToThemeViewer call through to navigator', () async { - var nav = _MockNav(); + var nav = MockQuickNav(); await setupTestLocator((locator) { locator.registerLazySingleton(() => nav); }); - var context = _MockContext(); + var context = MockBuildContext(); SettingsInteractor().routeToThemeViewer(context); var screen = verify(nav.push(context, captureAny)).captured[0]; @@ -46,21 +47,33 @@ void main() { }); test('routeToRemoteConfig call through to navigator', () async { - var nav = _MockNav(); + var nav = MockQuickNav(); await setupTestLocator((locator) { locator.registerLazySingleton(() => nav); }); - var context = _MockContext(); + var context = MockBuildContext(); SettingsInteractor().routeToRemoteConfig(context); var screen = verify(nav.push(context, captureAny)).captured[0]; expect(screen, isA()); }); + test('routeToLegal call through to navigator', () async { + var nav = MockQuickNav(); + await setupTestLocator((locator) { + locator.registerLazySingleton(() => nav); + }); + + var context = MockBuildContext(); + SettingsInteractor().routeToLegal(context); + + verify(nav.pushRoute(any, argThat(matches(PandaRouter.legal())))); + }); + testNonWidgetsWithContext('toggle dark mode sets dark mode to true', (tester) async { await setupPlatformChannels(); - final analytics = _MockAnalytics(); + final analytics = MockAnalytics(); await setupTestLocator((locator) => locator.registerLazySingleton(() => analytics)); @@ -68,13 +81,13 @@ void main() { await tester.pumpAndSettle(); final context = tester.state(find.byType(MaterialApp)).context; - expect(ParentTheme.of(context).isDarkMode, false); + expect(ParentTheme.of(context)?.isDarkMode, false); SettingsInteractor().toggleDarkMode(context, null); - expect(ParentTheme.of(context).isDarkMode, true); + expect(ParentTheme.of(context)?.isDarkMode, true); SettingsInteractor().toggleDarkMode(context, null); - expect(ParentTheme.of(context).isDarkMode, false); + expect(ParentTheme.of(context)?.isDarkMode, false); verify(analytics.logEvent(AnalyticsEventConstants.DARK_MODE_OFF)).called(1); verify(analytics.logEvent(AnalyticsEventConstants.DARK_MODE_ON)).called(1); @@ -82,7 +95,7 @@ void main() { testNonWidgetsWithContext('toggle hc mode sets hc mode to true', (tester) async { await setupPlatformChannels(); - final analytics = _MockAnalytics(); + final analytics = MockAnalytics(); await setupTestLocator((locator) => locator.registerLazySingleton(() => analytics)); @@ -90,21 +103,15 @@ void main() { await tester.pumpAndSettle(); final context = tester.state(find.byType(MaterialApp)).context; - expect(ParentTheme.of(context).isHC, false); + expect(ParentTheme.of(context)?.isHC, false); SettingsInteractor().toggleHCMode(context); - expect(ParentTheme.of(context).isHC, true); + expect(ParentTheme.of(context)?.isHC, true); SettingsInteractor().toggleHCMode(context); - expect(ParentTheme.of(context).isHC, false); + expect(ParentTheme.of(context)?.isHC, false); verify(analytics.logEvent(AnalyticsEventConstants.HC_MODE_OFF)).called(1); verify(analytics.logEvent(AnalyticsEventConstants.HC_MODE_ON)).called(1); }); -} - -class _MockNav extends Mock implements QuickNav {} - -class _MockContext extends Mock implements BuildContext {} - -class _MockAnalytics extends Mock implements Analytics {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/settings/settings_screen_test.dart b/apps/flutter_parent/test/screens/settings/settings_screen_test.dart index 59fadba746..2b28e6a3da 100644 --- a/apps/flutter_parent/test/screens/settings/settings_screen_test.dart +++ b/apps/flutter_parent/test/screens/settings/settings_screen_test.dart @@ -13,7 +13,6 @@ // along with this program. If not, see . import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/screens/settings/settings_interactor.dart'; @@ -24,11 +23,12 @@ import 'package:mockito/mockito.dart'; import '../../utils/accessibility_utils.dart'; import '../../utils/test_app.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; import '../../utils/test_utils.dart'; void main() { - SettingsInteractor interactor; - final analytics = _MockAnalytics(); + late MockSettingsInteractor interactor; + final analytics = MockAnalytics(); AppLocalizations l10n = AppLocalizations(); themeViewerButton() => find.byKey(Key('theme-viewer')); @@ -39,13 +39,13 @@ void main() { webViewDarkModeToggle() => find.text(l10n.webViewDarkModeLabel); setUpAll(() { - interactor = _MockInteractor(); + interactor = MockSettingsInteractor(); when(interactor.isDebugMode()).thenReturn(true); when(interactor.toggleDarkMode(any, any)).thenAnswer((invocation) { - ParentTheme.of(invocation.positionalArguments[0]).toggleDarkMode(); + ParentTheme.of(invocation.positionalArguments[0])?.toggleDarkMode(); }); when(interactor.toggleHCMode(any)).thenAnswer((invocation) { - ParentTheme.of(invocation.positionalArguments[0]).toggleHC(); + ParentTheme.of(invocation.positionalArguments[0])?.toggleHC(); }); setupTestLocator((locator) { locator.registerFactory(() => interactor); @@ -57,6 +57,18 @@ void main() { reset(analytics); }); + testWidgetsWithAccessibilityChecks('Displays about button', (tester) async { + await tester.pumpWidget(TestApp(SettingsScreen())); + await tester.pumpAndSettle(); + expect(find.text(l10n.about), findsOneWidget); + }); + + testWidgetsWithAccessibilityChecks('Displays legal button', (tester) async { + await tester.pumpWidget(TestApp(SettingsScreen())); + await tester.pumpAndSettle(); + expect(find.text(l10n.helpLegalLabel), findsOneWidget); + }); + testWidgetsWithAccessibilityChecks('Displays theme viewer button in debug mode', (tester) async { await tester.pumpWidget(TestApp(SettingsScreen())); await tester.pumpAndSettle(); @@ -120,13 +132,13 @@ void main() { await tester.pumpAndSettle(); var state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isDarkMode, isFalse); + expect(ParentTheme.of(state.context)?.isDarkMode, isFalse); await tester.tap(darkModeButton()); await tester.pumpAndSettle(); state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isDarkMode, isTrue); + expect(ParentTheme.of(state.context)?.isDarkMode, isTrue); }); testWidgetsWithAccessibilityChecks('Switches to light mode', (tester) async { @@ -134,13 +146,13 @@ void main() { await tester.pumpAndSettle(); var state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isDarkMode, isTrue); + expect(ParentTheme.of(state.context)?.isDarkMode, isTrue); await tester.tap(lightModeButton()); await tester.pumpAndSettle(); state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isDarkMode, isFalse); + expect(ParentTheme.of(state.context)?.isDarkMode, isFalse); }); testWidgetsWithAccessibilityChecks('Enables high contrast mode', (tester) async { @@ -148,13 +160,13 @@ void main() { await tester.pumpAndSettle(); var state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isHC, isFalse); + expect(ParentTheme.of(state.context)?.isHC, isFalse); await tester.tap(hcToggle()); await tester.pumpAndSettle(); state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isHC, isTrue); + expect(ParentTheme.of(state.context)?.isHC, isTrue); }); testWidgetsWithAccessibilityChecks('Hides WebView dark mode toggle in light mode', (tester) async { @@ -176,25 +188,21 @@ void main() { await tester.pumpAndSettle(); var state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isWebViewDarkMode, isFalse); + expect(ParentTheme.of(state.context)?.isWebViewDarkMode, isFalse); await tester.tap(webViewDarkModeToggle()); await tester.pumpAndSettle(); state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isWebViewDarkMode, isTrue); + expect(ParentTheme.of(state.context)?.isWebViewDarkMode, isTrue); await tester.tap(webViewDarkModeToggle()); await tester.pumpAndSettle(); state = tester.state(find.byType(SettingsScreen)); - expect(ParentTheme.of(state.context).isWebViewDarkMode, isFalse); + expect(ParentTheme.of(state.context)?.isWebViewDarkMode, isFalse); verify(analytics.logEvent(AnalyticsEventConstants.DARK_WEB_MODE_ON)).called(1); verify(analytics.logEvent(AnalyticsEventConstants.DARK_WEB_MODE_OFF)).called(1); }); -} - -class _MockInteractor extends Mock implements SettingsInteractor {} - -class _MockAnalytics extends Mock implements Analytics {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/splash/splash_screen_interactor_test.dart b/apps/flutter_parent/test/screens/splash/splash_screen_interactor_test.dart index 20cdef836b..a1b0628c92 100644 --- a/apps/flutter_parent/test/screens/splash/splash_screen_interactor_test.dart +++ b/apps/flutter_parent/test/screens/splash/splash_screen_interactor_test.dart @@ -35,15 +35,15 @@ import 'package:test/test.dart'; import '../../utils/canvas_model_utils.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; -import '../dashboard/dashboard_interactor_test.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { - _MockDashboardInteractor dashboardInteractor = _MockDashboardInteractor(); - _MockAccountsApi accountsApi = _MockAccountsApi(); - _MockAuthApi authApi = _MockAuthApi(); - final mockScanner = MockBarcodeScanner(); + MockDashboardInteractor dashboardInteractor = MockDashboardInteractor(); + MockAccountsApi accountsApi = MockAccountsApi(); + MockAuthApi authApi = MockAuthApi(); + final mockScanner = MockBarcodeScanVeneer(); MockUserApi userApi = MockUserApi(); - _MockFeaturesApi featuresApi = _MockFeaturesApi(); + MockFeaturesApi featuresApi = MockFeaturesApi(); Login login = Login((b) => b ..domain = 'domain' @@ -121,19 +121,19 @@ void main() { verifyNever(accountsApi.getAccountPermissions()); // canMasquerade should be set to true - expect(ApiPrefs.getCurrentLogin().canMasquerade, isTrue); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isTrue); }); test('Sets canMasquerade to false if getAccountPermissions returns false', () async { ApiPrefs.switchLogins(login); // canMasquerade should not be set at this point - expect(ApiPrefs.getCurrentLogin().canMasquerade, isNull); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isNull); await SplashScreenInteractor().getData(); // canMasquerade should now be set to false - expect(ApiPrefs.getCurrentLogin().canMasquerade, isFalse); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isFalse); }); test('Sets canMasquerade to true if getAccountPermissions returns true', () async { @@ -141,12 +141,12 @@ void main() { ApiPrefs.switchLogins(login); // canMasquerade should not be set at this point - expect(ApiPrefs.getCurrentLogin().canMasquerade, isNull); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isNull); await SplashScreenInteractor().getData(); // canMasquerade should now be set to false - expect(ApiPrefs.getCurrentLogin().canMasquerade, isTrue); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isTrue); }); test('Sets canMasquerade to false if getAccountPermissions call fails', () async { @@ -154,12 +154,12 @@ void main() { ApiPrefs.switchLogins(login); // canMasquerade should not be set at this point - expect(ApiPrefs.getCurrentLogin().canMasquerade, isNull); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isNull); await SplashScreenInteractor().getData(); // canMasquerade should now be set to false - expect(ApiPrefs.getCurrentLogin().canMasquerade, isFalse); + expect(ApiPrefs.getCurrentLogin()?.canMasquerade, isFalse); }); test('getData returns false for isObserver if user is not observing any students', () async { @@ -167,7 +167,7 @@ void main() { var data = await SplashScreenInteractor().getData(); // isObserver should be false - expect(data.isObserver, isFalse); + expect(data?.isObserver, isFalse); }); test('getData returns true for isObserver if user is observing students', () async { @@ -179,7 +179,7 @@ void main() { var data = await SplashScreenInteractor().getData(); // isObserver should be true - expect(data.isObserver, isTrue); + expect(data?.isObserver, isTrue); }); test('getData should return existing value for canMasquerade', () async { @@ -187,13 +187,14 @@ void main() { var data = await SplashScreenInteractor().getData(); // canMasquerade should be true - expect(data.canMasquerade, isTrue); + expect(data?.canMasquerade, isTrue); }); test('getData returns QRLoginError for invalid qrLoginUrl', () async { bool fail = false; await SplashScreenInteractor().getData(qrLoginUrl: 'https://hodor.com').catchError((_) { fail = true; // Don't return, just update the flag + return Future.value(null); }); expect(fail, isTrue); }); @@ -206,8 +207,8 @@ void main() { ApiPrefs.switchLogins(login); final url = 'https://sso.canvaslms.com/canvas/login?code_android_parent=1234&domain=mobiledev.instructure.com'; var data = await SplashScreenInteractor().getData(qrLoginUrl: url); - expect(data.isObserver, isTrue); - expect(data.canMasquerade, isFalse); + expect(data?.isObserver, isTrue); + expect(data?.canMasquerade, isFalse); }); test('getData returns valid data for valid qrLoginUrl, canMasquerade true for real user', () async { @@ -224,8 +225,8 @@ void main() { ApiPrefs.switchLogins(login); final url = 'https://sso.canvaslms.com/canvas/login?code_android_parent=1234&domain=mobiledev.instructure.com'; var data = await SplashScreenInteractor().getData(qrLoginUrl: url); - expect(data.isObserver, isTrue); - expect(data.canMasquerade, isTrue); + expect(data?.isObserver, isTrue); + expect(data?.canMasquerade, isTrue); }); test('getData returns QRLoginError for invalid auth code', () async { @@ -234,6 +235,7 @@ void main() { bool fail = false; await SplashScreenInteractor().getData(qrLoginUrl: url).catchError((_) { fail = true; // Don't return, just update the flag + return Future.value(null); }); expect(fail, isTrue); }); @@ -244,6 +246,7 @@ void main() { bool fail = false; await SplashScreenInteractor().getData(qrLoginUrl: url).catchError((_) { fail = true; // Don't return, just update the flag + return Future.value(null); }); expect(fail, isTrue); }); @@ -270,12 +273,4 @@ void main() { var db = (locator() as MockUserColorsDb); verify(db.insertOrUpdateAll(login.domain, login.user.id, expectedColors)); }); -} - -class _MockAccountsApi extends Mock implements AccountsApi {} - -class _MockDashboardInteractor extends Mock implements DashboardInteractor {} - -class _MockAuthApi extends Mock implements AuthApi {} - -class _MockFeaturesApi extends Mock implements FeaturesApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/splash/splash_screen_test.dart b/apps/flutter_parent/test/screens/splash/splash_screen_test.dart index b248c1e07c..1e0e7c9898 100644 --- a/apps/flutter_parent/test/screens/splash/splash_screen_test.dart +++ b/apps/flutter_parent/test/screens/splash/splash_screen_test.dart @@ -33,6 +33,7 @@ import '../../utils/canvas_model_utils.dart'; import '../../utils/platform_config.dart'; import '../../utils/test_app.dart'; import '../../utils/test_helpers/mock_helpers.dart'; +import '../../utils/test_helpers/mock_helpers.mocks.dart'; void main() { final login = Login((b) => b @@ -50,8 +51,8 @@ void main() { }); testWidgetsWithAccessibilityChecks('Displays loadingIndicator', (tester) async { - var interactor = _MockInteractor(); - var nav = _MockNav(); + var interactor = MockSplashScreenInteractor(); + var nav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => nav); @@ -70,7 +71,7 @@ void main() { testWidgetsWithAccessibilityChecks('Routes to not-a-parent screen if not an observer and cannot masquerade', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockSplashScreenInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => QuickNav()); @@ -92,8 +93,8 @@ void main() { }); testWidgetsWithAccessibilityChecks('Routes to Acceptable Use Policy screen if needed', (tester) async { - var interactor = _MockInteractor(); - var mockNav = _MockNav(); + var interactor = MockSplashScreenInteractor(); + var mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => mockNav); @@ -116,8 +117,8 @@ void main() { }); testWidgetsWithAccessibilityChecks('Routes to dashboard if not an observer but can masquerade', (tester) async { - var interactor = _MockInteractor(); - var mockNav = _MockNav(); + var interactor = MockSplashScreenInteractor(); + var mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => mockNav); @@ -136,12 +137,12 @@ void main() { await tester.pump(const Duration(milliseconds: 350)); // Pump for animation finish verify(mockNav.pushRouteWithCustomTransition(any, '/dashboard', any, any, any)); - ApiPrefs.clean(); + await ApiPrefs.clean(); }); testWidgetsWithAccessibilityChecks('Routes to dashboard if an observer but cannot masquerade', (tester) async { - var interactor = _MockInteractor(); - var mockNav = _MockNav(); + var interactor = MockSplashScreenInteractor(); + var mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => mockNav); @@ -160,11 +161,11 @@ void main() { await tester.pump(const Duration(milliseconds: 350)); // Pump for animation finish verify(mockNav.pushRouteWithCustomTransition(any, '/dashboard', any, any, any)); - ApiPrefs.clean(); + await ApiPrefs.clean(); }); testWidgetsWithAccessibilityChecks('Routes to login screen when the user is not logged in', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockSplashScreenInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => QuickNav()); @@ -180,7 +181,7 @@ void main() { testWidgetsWithAccessibilityChecks( 'Routes to login screen when the user is not logged in and camera count throws error', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockSplashScreenInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => QuickNav()); @@ -224,8 +225,8 @@ void main() { */ testWidgetsWithAccessibilityChecks('Routes to dashboard without students on error', (tester) async { - var interactor = _MockInteractor(); - var mockNav = _MockNav(); + var interactor = MockSplashScreenInteractor(); + var mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => mockNav); @@ -247,7 +248,7 @@ void main() { await tester.pump(const Duration(milliseconds: 350)); verify(mockNav.pushRouteWithCustomTransition(any, '/dashboard', any, any, any)); - ApiPrefs.clean(); + await ApiPrefs.clean(); }); testWidgetsWithAccessibilityChecks('Requests MasqueradeUI refresh', (tester) async { @@ -257,8 +258,8 @@ void main() { ..masqueradeUser = masqueradeUser.toBuilder()); var masqueradeInfo = AppLocalizations().actingAsUser(masqueradeUser.name); - var interactor = _MockInteractor(); - var mockNav = _MockNav(); + var interactor = MockSplashScreenInteractor(); + var mockNav = MockQuickNav(); setupTestLocator((locator) { locator.registerFactory(() => interactor); locator.registerLazySingleton(() => mockNav); @@ -285,10 +286,6 @@ void main() { // Should now show masquerade info expect(find.text(masqueradeInfo), findsOneWidget); - ApiPrefs.clean(); + await ApiPrefs.clean(); }); -} - -class _MockInteractor extends Mock implements SplashScreenInteractor {} - -class _MockNav extends Mock implements QuickNav {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/screens/theme_viewer_screen_test.dart b/apps/flutter_parent/test/screens/theme_viewer_screen_test.dart index f1f675000f..00f1ba4f79 100644 --- a/apps/flutter_parent/test/screens/theme_viewer_screen_test.dart +++ b/apps/flutter_parent/test/screens/theme_viewer_screen_test.dart @@ -33,45 +33,45 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - ThemeViewerScreen.scaffoldKey.currentState.openDrawer(); + ThemeViewerScreen.scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Light mode, normal contrast. Title should be dark, subtitle should be gray. - expect(tester.widget(title()).style.color, ParentColors.licorice); - expect(tester.widget(subtitle()).style.color, ParentColors.ash); + expect(tester.widget(title()).style!.color, ParentColors.licorice); + expect(tester.widget(subtitle()).style!.color, ParentColors.oxford); // Enable dark mode await tester.tap(darkToggle()); await tester.pumpAndSettle(); // Dark mode, normal contrast. Title should be light, subtitle should be gray. - expect(tester.widget(title()).style.color, ParentColors.tiara); - expect(tester.widget(subtitle()).style.color, ParentColors.ash); + expect(tester.widget(title()).style!.color, ParentColors.tiara); + expect(tester.widget(subtitle()).style!.color, ParentColors.ash); // Enable High-Contrast mode await tester.tap(hcToggle()); await tester.pumpAndSettle(); // Dark mode, high contrast. Both title and subtitle should be light - expect(tester.widget(title()).style.color, ParentColors.tiara); - expect(tester.widget(subtitle()).style.color, ParentColors.tiara); + expect(tester.widget(title()).style!.color, ParentColors.tiara); + expect(tester.widget(subtitle()).style!.color, ParentColors.tiara); // Disable dark mode await tester.tap(darkToggle()); await tester.pumpAndSettle(); // Light mode, high contrast. Both title and subtitle should be dark - expect(tester.widget(title()).style.color, ParentColors.licorice); - expect(tester.widget(subtitle()).style.color, ParentColors.licorice); + expect(tester.widget(title()).style!.color, ParentColors.licorice); + expect(tester.widget(subtitle()).style!.color, ParentColors.licorice); }); testWidgets('Set and returns correct values for dark and high-contrast modes', (tester) async { await tester.pumpWidget(TestApp(ThemeViewerScreen())); await tester.pumpAndSettle(); - var state = ParentTheme.of(ThemeViewerScreen.scaffoldKey.currentContext); + var state = ParentTheme.of(ThemeViewerScreen.scaffoldKey.currentContext!); - state.isDarkMode = false; + state!.isDarkMode = false; state.isHC = false; expect(state.isLightNormal, isTrue); expect(state.isLightHC, isFalse); @@ -106,7 +106,7 @@ void main() { await tester.pumpAndSettle(); // Open the drawer - ThemeViewerScreen.scaffoldKey.currentState.openDrawer(); + ThemeViewerScreen.scaffoldKey.currentState?.openDrawer(); await tester.pumpAndSettle(); // Switch student color to 'raspberry' @@ -116,7 +116,7 @@ void main() { await tester.pumpAndSettle(); StudentColorSet expected = StudentColorSet.raspberry; - Color actualColor() => tester.widget(studentColor()).color; + Color actualColor() => tester.widget(studentColor()).color!; // Light mode, normal contrast. expect(actualColor(), expected.light); diff --git a/apps/flutter_parent/test/utils/accessibility_utils.dart b/apps/flutter_parent/test/utils/accessibility_utils.dart index 0ca40e3e50..0d98f6e91b 100644 --- a/apps/flutter_parent/test/utils/accessibility_utils.dart +++ b/apps/flutter_parent/test/utils/accessibility_utils.dart @@ -24,7 +24,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart' as test_package; -import 'package:test_api/src/frontend/async_matcher.dart'; /// Exclusions for the [AccessibilityGuideline] checks. /// @@ -46,15 +45,11 @@ extension A11yExclusionExtension on A11yExclusion { switch (this) { case A11yExclusion.multipleNodesWithSameLabel: return 'Multiple nodes with the same label'; - break; case A11yExclusion.minContrastRatio: return 'Expected contrast ratio of at least'; - break; case A11yExclusion.minTapSize: return 'expected tap target size of at least'; - break; } - return null; } } @@ -75,8 +70,7 @@ void testWidgetsWithAccessibilityChecks( String description, WidgetTesterCallback callback, { bool skip = false, - test_package.Timeout timeout, - Duration initialTimeout, + test_package.Timeout? timeout, bool semanticsEnabled = true, Set a11yExclusions = const {}, }) { @@ -84,11 +78,11 @@ void testWidgetsWithAccessibilityChecks( testWidgets(description, (tester) async { if (envVars["deviceWidth"] != null && envVars["deviceHeight"] != null) { - var width = double.parse(envVars["deviceWidth"]); - var height = double.parse(envVars["deviceHeight"]); + var width = double.parse(envVars["deviceWidth"]!); + var height = double.parse(envVars["deviceHeight"]!); double ratio = 1.0; if (envVars["pixelRatio"] != null) { - ratio = double.parse(envVars["pixelRatio"]); + ratio = double.parse(envVars["pixelRatio"]!); } print("Changing device res to width=$width, height=$height, ratio=$ratio"); @@ -99,19 +93,19 @@ void testWidgetsWithAccessibilityChecks( await callback(tester); // Run our accessibility test suite at the end of the test. - await runAccessibilityTests(tester, a11yExclusions); + await runAccessibilityTests(tester); handle.dispose(); - }, skip: skip, timeout: timeout, initialTimeout: initialTimeout, semanticsEnabled: semanticsEnabled); + }, skip: skip, timeout: timeout, semanticsEnabled: semanticsEnabled); } // Break this out into its own method, so that it can be used mid-test. -Future runAccessibilityTests(WidgetTester tester, Set exclusions) async { - await expectLater(tester, meetsGuidelineWithExclusions(textContrastGuideline, exclusions)); - await expectLater(tester, meetsGuidelineWithExclusions(labeledTapTargetGuideline, exclusions)); - await expectLater(tester, meetsGuidelineWithExclusions(androidTapTargetGuideline, exclusions)); +Future runAccessibilityTests(WidgetTester tester) async { + await expectLater(tester, meetsGuideline(textContrastGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); // Needs to be last, because it fiddles with UI - await expectLater(tester, meetsGuidelineWithExclusions(TextFieldNavigationGuideline(), exclusions)); + await expectLater(tester, meetsGuideline(TextFieldNavigationGuideline())); } // Here's an example of a custom guideline. We can conceivably write @@ -125,7 +119,7 @@ class NoHintsGuideline extends AccessibilityGuideline { @override FutureOr evaluate(WidgetTester tester) { - final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; + final SemanticsNode? root = tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode; // Traversal logic that recurses to children Evaluation traverse(SemanticsNode node) { @@ -135,7 +129,7 @@ class NoHintsGuideline extends AccessibilityGuideline { return true; }); - if (node.hint != null && node.hint.isNotEmpty) { + if (node.hint.isNotEmpty) { //print("Node $node hint = ${node.hint}"); result += Evaluation.fail('$node has hint \'${node.hint}\'!\n'); } @@ -143,7 +137,7 @@ class NoHintsGuideline extends AccessibilityGuideline { return result; // Returns aggregate result } - return traverse(root); // Start traversing at the root. + return traverse(root!); // Start traversing at the root. } } @@ -156,7 +150,7 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { // Grab the focusable SemanticsNodes associated with this screen. List _getFocusableSemanticsNodes(SemanticsNode root) { - List result = List(); + List result = []; if (root.hasFlag(SemanticsFlag.isFocusable) && !root.isMergedIntoParent && !root.isInvisible && @@ -178,7 +172,7 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { return root; } - SemanticsNode result = null; + SemanticsNode? result = null; root.visitChildren((SemanticsNode child) { if (result == null) { result = _findFocusedNode(child); @@ -186,7 +180,7 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { return true; }); - return result; + return result!; } // @@ -217,7 +211,7 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { Future _move(WidgetTester tester, LogicalKeyboardKey key) async { await tester.sendKeyEvent(key); await tester.pumpAndSettle(); - FocusNode newFocus = tester.binding.focusManager.primaryFocus; + FocusNode newFocus = tester.binding.focusManager.primaryFocus!; return newFocus; } @@ -225,17 +219,17 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { @override FutureOr evaluate(WidgetTester tester) async { - final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; + final SemanticsNode? root = tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode; // Default result Evaluation result = Evaluation.pass(); // Gather all focusable SemanticsNodes for our screen // We can't get this info through the focus node tree. - List focusableSemanticsNodes = _getFocusableSemanticsNodes(root); + List focusableSemanticsNodes = _getFocusableSemanticsNodes(root!); Iterable editableTextFocusNodes = tester.binding.focusManager.rootScope.descendants - .where((fn) => fn.context != null && fn.context.widget is EditableText); + .where((fn) => fn.context != null && fn.context?.widget is EditableText); if (focusableSemanticsNodes.length > 1) { // Only test navigability if there is something else to which to navigate. @@ -245,7 +239,7 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { await tester.pumpAndSettle(); // and then try to navigate out of it. - FocusNode currFocus = tester.binding.focusManager.primaryFocus; + FocusNode? currFocus = tester.binding.focusManager.primaryFocus; FocusNode newFocus = await _moveUp(tester); if (newFocus == currFocus) { newFocus = await _moveDown(tester); @@ -267,33 +261,3 @@ class TextFieldNavigationGuideline extends AccessibilityGuideline { return result; } } - -AsyncMatcher meetsGuidelineWithExclusions(AccessibilityGuideline guideline, Set exclusions) { - return _MatchesAccessibilityGuidelineWithExclusions(guideline, exclusions); -} - -class _MatchesAccessibilityGuidelineWithExclusions extends AsyncMatcher { - _MatchesAccessibilityGuidelineWithExclusions(this.guideline, this.exclusions); - - final AccessibilityGuideline guideline; - final Set exclusions; - - @override - Description describe(Description description) { - return description.add(guideline.description); - } - - @override - Future matchAsync(covariant WidgetTester tester) async { - final Evaluation result = await guideline.evaluate(tester); - if (result.passed) return null; - for (var exclusion in exclusions) { - if (result.reason.contains(exclusion.errorMessageContents)) { - print('A11y check failure ignored because $exclusion was applied:'); - print(' - ${result.reason}'); - return null; - } - } - return result.reason; - } -} diff --git a/apps/flutter_parent/test/utils/alert_helper_test.dart b/apps/flutter_parent/test/utils/alert_helper_test.dart index d4bc9280b0..ea3fc1369f 100644 --- a/apps/flutter_parent/test/utils/alert_helper_test.dart +++ b/apps/flutter_parent/test/utils/alert_helper_test.dart @@ -22,6 +22,7 @@ import 'package:mockito/mockito.dart'; import 'test_app.dart'; import 'test_helpers/mock_helpers.dart'; +import 'test_helpers/mock_helpers.mocks.dart'; void main() { final courseApi = MockCourseApi(); diff --git a/apps/flutter_parent/test/utils/canvas_model_utils.dart b/apps/flutter_parent/test/utils/canvas_model_utils.dart index 0f38fca4d7..26b9da28ec 100644 --- a/apps/flutter_parent/test/utils/canvas_model_utils.dart +++ b/apps/flutter_parent/test/utils/canvas_model_utils.dart @@ -17,7 +17,7 @@ import 'dart:math'; import 'package:flutter_parent/models/user.dart'; class CanvasModelTestUtils { - static User mockUser({String name, String pronouns, String primaryEmail, String id, String shortName}) => + static User mockUser({String? name, String? pronouns, String? primaryEmail, String? id, String? shortName}) => User((b) => b ..id = id ?? Random(name.hashCode).nextInt(100000).toString() ..sortableName = name ?? 'sortableName' diff --git a/apps/flutter_parent/test/utils/core_extensions/date_time_extensions_test.dart b/apps/flutter_parent/test/utils/core_extensions/date_time_extensions_test.dart index ea2e397908..7f477120e3 100644 --- a/apps/flutter_parent/test/utils/core_extensions/date_time_extensions_test.dart +++ b/apps/flutter_parent/test/utils/core_extensions/date_time_extensions_test.dart @@ -29,7 +29,7 @@ void main() { String expectedDefaultOutput = localize(expectedDefaultDate, expectedDefaultTime); test('returns null if DateTime is null', () { - DateTime date = null; + DateTime? date = null; expect(date.l10nFormat(localize), isNull); }); @@ -52,14 +52,14 @@ void main() { test('applies specified date format', () { String expectedDate = DateFormat.MMMMEEEEd().format(now); String expected = localize(expectedDate, expectedDefaultTime); - String actual = now.l10nFormat(localize, dateFormat: DateFormat.MMMMEEEEd()); + String? actual = now.l10nFormat(localize, dateFormat: DateFormat.MMMMEEEEd()); expect(actual, expected); }); test('applies specified time format', () { String expectedTime = DateFormat.MMMMEEEEd().format(now); String expected = localize(expectedDefaultDate, expectedTime); - String actual = now.l10nFormat(localize, timeFormat: DateFormat.MMMMEEEEd()); + String? actual = now.l10nFormat(localize, timeFormat: DateFormat.MMMMEEEEd()); expect(actual, expected); }); }); @@ -71,20 +71,20 @@ void main() { }); test('returns false if this date is null', () { - DateTime date1 = null; + DateTime? date1 = null; DateTime date2 = DateTime.now(); expect(date1.isSameDayAs(date2), isFalse); }); test('returns false if other date is null', () { DateTime date1 = DateTime.now(); - DateTime date2 = null; + DateTime? date2 = null; expect(date1.isSameDayAs(date2), isFalse); }); test('returns false if both dates are null', () { - DateTime date1 = null; - DateTime date2 = null; + DateTime? date1 = null; + DateTime? date2 = null; expect(date1.isSameDayAs(date2), isFalse); }); @@ -109,7 +109,7 @@ void main() { }); test('returns null if date is null', () { - DateTime date = null; + DateTime? date = null; expect(date.withFirstDayOfWeek(), isNull); }); @@ -170,7 +170,7 @@ void main() { }); test('returns false if date is null', () { - DateTime date = null; + DateTime? date = null; expect(date.isWeekend(), isFalse); }); diff --git a/apps/flutter_parent/test/utils/core_extensions/list_extensions_test.dart b/apps/flutter_parent/test/utils/core_extensions/list_extensions_test.dart index c55e21cd89..884f09380c 100644 --- a/apps/flutter_parent/test/utils/core_extensions/list_extensions_test.dart +++ b/apps/flutter_parent/test/utils/core_extensions/list_extensions_test.dart @@ -18,22 +18,22 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('sortBy', () { test('sortBy returns null if list is null', () { - final List unsorted = null; - final List actual = unsorted.sortBy([(it) => it]); + final List? unsorted = null; + final List? actual = unsorted.sortBySelector([(it) => it]); expect(actual, isNull); }); test('sortBy correctly sorts in ascending order', () { final List unsorted = [3, 4, 2, 5, 1]; final List expected = [1, 2, 3, 4, 5]; - final List actual = unsorted.sortBy([(it) => it]); + final List? actual = unsorted.sortBySelector([(it) => it]); expect(actual, expected); }); test('sortBy correctly sorts in descending order', () { final List unsorted = [3, 4, 2, 5, 1]; final List expected = [5, 4, 3, 2, 1]; - final List actual = unsorted.sortBy([(it) => it], descending: true); + final List? actual = unsorted.sortBySelector([(it) => it], descending: true); expect(actual, expected); }); @@ -44,7 +44,7 @@ void main() { _TestClass(number: null), ]; expect( - () => unsorted.sortBy([(it) => it.number], nullSortOrder: NullSortOrder.none), + () => unsorted.sortBySelector([(it) => it?.number], nullSortOrder: NullSortOrder.none), throwsArgumentError, ); }); @@ -56,7 +56,7 @@ void main() { _TestClass(number: null), ]; expect( - unsorted.sortBy([(it) => it.number], nullSortOrder: NullSortOrder.none), + unsorted.sortBySelector([(it) => it?.number], nullSortOrder: NullSortOrder.none), unsorted, ); }); @@ -68,7 +68,7 @@ void main() { _TestClass(number: null, text: '3'), ]; final expected = [unsorted[1], unsorted[0], unsorted[2]]; - final actual = unsorted.sortBy([(it) => it.number, (it) => it.text], nullSortOrder: NullSortOrder.greaterThan); + final actual = unsorted.sortBySelector([(it) => it?.number, (it) => it?.text], nullSortOrder: NullSortOrder.greaterThan); expect(actual, expected); }); @@ -79,7 +79,7 @@ void main() { _TestClass(number: null, text: '3'), ]; final expected = [unsorted[0], unsorted[2], unsorted[1]]; - final actual = unsorted.sortBy([(it) => it.number, (it) => it.text], nullSortOrder: NullSortOrder.lessThan); + final actual = unsorted.sortBySelector([(it) => it?.number, (it) => it?.text], nullSortOrder: NullSortOrder.lessThan); expect(actual, expected); }); @@ -90,7 +90,7 @@ void main() { _TestClass(number: null, text: '3'), ]; final expected = [unsorted[0], unsorted[1], unsorted[2]]; - final actual = unsorted.sortBy([(it) => it.number, (it) => it.text], nullSortOrder: NullSortOrder.equal); + final actual = unsorted.sortBySelector([(it) => it?.number, (it) => it?.text], nullSortOrder: NullSortOrder.equal); expect(actual, expected); }); @@ -105,7 +105,7 @@ void main() { _TestClass(number: 7, text: '7'), ]; final expected = [unsorted[0], unsorted[2], unsorted[4], unsorted[6], unsorted[1], unsorted[3], unsorted[5]]; - final actual = unsorted.sortBy([(it) => it.number]); + final actual = unsorted.sortBySelector([(it) => it?.number]); expect(actual, expected); }); @@ -121,7 +121,7 @@ void main() { _TestClass(date: now, number: 123, text: '5'), ]; final expected = [unsorted[4], unsorted[3], unsorted[1], unsorted[2], unsorted[0], unsorted[6], unsorted[5]]; - final actual = unsorted.sortBy([(it) => it.date, (it) => it.number, (it) => it.text]); + final actual = unsorted.sortBySelector([(it) => it?.date, (it) => it?.number, (it) => it?.text]); expect(actual, expected); }); }); @@ -129,7 +129,7 @@ void main() { group('count', () { test('count returns 0 for empty list', () { List<_TestClass> list = []; - expect(list.count((it) => it.number < 5), 0); + expect(list.count((it) => it?.number != null ? false : it!.number! < 5), 0); }); test('count returns 0 if predicate is always false', () { @@ -138,13 +138,13 @@ void main() { }); test('count returns 0 if list is null', () { - List<_TestClass> list = null; + List<_TestClass>? list = null; expect(list.count((_) => false), 0); }); test('count returns correct count based on predicate', () { List<_TestClass> list = List<_TestClass>.generate(100, (index) => _TestClass(number: index)); - expect(list.count((it) => it.number < 50), 50); + expect(list.count((it) => it?.number == null ? false : it!.number! < 50), 50); }); test('count returns list size if predicate is always true', () { @@ -158,26 +158,26 @@ void main() { List original = ['', 'A', 'AB', 'ABC']; List expected = ['0', '1', '2', '3']; - List actual = original.mapIndexed((index, item) { + List? actual = original.mapIndexed((index, item) { expect(item, original[index]); - return item.length.toString(); + return item?.length.toString(); }); expect(actual, expected); }); test('Returns null if list is null', () { - List original = null; - List actual = original.mapIndexed((index, item) => ''); + List? original = null; + List? actual = original.mapIndexed((index, item) => ''); expect(actual, isNull); }); }); } class _TestClass { - final int number; - final String text; - final DateTime date; + final int? number; + final String? text; + final DateTime? date; _TestClass({this.number, this.text, this.date}); diff --git a/apps/flutter_parent/test/utils/db/calendar_filter_db_test.dart b/apps/flutter_parent/test/utils/db/calendar_filter_db_test.dart index b28a01b476..d41ebb13f1 100644 --- a/apps/flutter_parent/test/utils/db/calendar_filter_db_test.dart +++ b/apps/flutter_parent/test/utils/db/calendar_filter_db_test.dart @@ -21,6 +21,7 @@ import 'package:test/test.dart'; import '../test_app.dart'; import '../test_helpers/mock_helpers.dart'; +import '../test_helpers/mock_helpers.mocks.dart'; void main() { final database = MockDatabase(); @@ -204,7 +205,7 @@ void main() { }); test('joinfilters returns empty string for null filter list', () { - Set filters = null; + Set? filters = null; final expected = ''; final actual = CalendarFilterDb.joinFilters(filters); expect(actual, expected); @@ -234,7 +235,7 @@ void main() { }); test('splitFilters returns empty list for null string', () { - String input = null; + String? input = null; List expected = []; final actual = CalendarFilterDb.splitFilters(input); expect(actual, expected); diff --git a/apps/flutter_parent/test/utils/db/reminder_db_test.dart b/apps/flutter_parent/test/utils/db/reminder_db_test.dart index 07b1d261b2..04691f2c96 100644 --- a/apps/flutter_parent/test/utils/db/reminder_db_test.dart +++ b/apps/flutter_parent/test/utils/db/reminder_db_test.dart @@ -20,6 +20,7 @@ import 'package:test/test.dart'; import '../test_app.dart'; import '../test_helpers/mock_helpers.dart'; +import '../test_helpers/mock_helpers.mocks.dart'; void main() { final database = MockDatabase(); diff --git a/apps/flutter_parent/test/utils/db/user_colors_db_test.dart b/apps/flutter_parent/test/utils/db/user_colors_db_test.dart index bbdca112e0..84be2733da 100644 --- a/apps/flutter_parent/test/utils/db/user_colors_db_test.dart +++ b/apps/flutter_parent/test/utils/db/user_colors_db_test.dart @@ -23,6 +23,7 @@ import 'package:test/test.dart'; import '../test_app.dart'; import '../test_helpers/mock_helpers.dart'; +import '../test_helpers/mock_helpers.mocks.dart'; void main() { final database = MockDatabase(); diff --git a/apps/flutter_parent/test/utils/network_image_response.dart b/apps/flutter_parent/test/utils/network_image_response.dart index 4bca8f5492..0dbd430888 100644 --- a/apps/flutter_parent/test/utils/network_image_response.dart +++ b/apps/flutter_parent/test/utils/network_image_response.dart @@ -21,6 +21,7 @@ import 'package:mockito/mockito.dart'; import 'package:transparent_image/transparent_image.dart'; import 'test_helpers/mock_helpers.dart'; +import 'test_helpers/mock_helpers.mocks.dart'; void mockNetworkImageResponse() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); @@ -32,7 +33,7 @@ void mockNetworkImageResponse() { class _ImageHttpOverrides extends HttpOverrides { @override - HttpClient createHttpClient(SecurityContext _) { + HttpClient createHttpClient(SecurityContext? _) { final client = MockHttpClient(); final request = MockHttpClientRequest(); final response = MockHttpClientResponse(); diff --git a/apps/flutter_parent/test/utils/notification_util_test.dart b/apps/flutter_parent/test/utils/notification_util_test.dart index 31d2bdb9f4..516e05cde3 100644 --- a/apps/flutter_parent/test/utils/notification_util_test.dart +++ b/apps/flutter_parent/test/utils/notification_util_test.dart @@ -27,10 +27,13 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import 'test_app.dart'; -import 'test_helpers/mock_helpers.dart'; +import 'test_helpers/mock_helpers.mocks.dart'; + +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; void main() { - final plugin = MockPlugin(); + final plugin = MockFlutterLocalNotificationsPlugin(); final database = MockReminderDb(); final analytics = MockAnalytics(); @@ -43,6 +46,7 @@ void main() { reset(plugin); reset(database); NotificationUtil.initForTest(plugin); + tz.initializeTimeZones(); }); test('initializes plugin with expected parameters', () async { @@ -50,14 +54,14 @@ void main() { final verification = verify(plugin.initialize( captureAny, - onSelectNotification: captureAnyNamed('onSelectNotification'), + onDidReceiveNotificationResponse: captureAnyNamed('onDidReceiveNotificationResponse'), )); InitializationSettings initSettings = verification.captured[0]; - expect(initSettings.android.defaultIcon, 'ic_notification_canvas_logo'); + expect(initSettings.android?.defaultIcon, 'ic_notification_canvas_logo'); expect(initSettings.iOS, null); - SelectNotificationCallback callback = verification.captured[1]; + var callback = verification.captured[1]; expect(callback, isNotNull); }); @@ -133,19 +137,24 @@ void main() { await NotificationUtil().scheduleReminder(AppLocalizations(), 'title', 'body', reminder); - final NotificationDetails details = verify(plugin.schedule( + tz.initializeTimeZones(); + var d = reminder.date!.toUtc(); + var date = tz.TZDateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second); + + final NotificationDetails details = verify(plugin.zonedSchedule( reminder.id, 'title', 'body', - reminder.date, + date, captureAny, payload: json.encode(serialize(expectedPayload)), + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, )).captured.first; expect(details.iOS, isNull); - expect(details.android.channelId, NotificationUtil.notificationChannelReminders); - expect(details.android.channelName, AppLocalizations().remindersNotificationChannelName); - expect(details.android.channelDescription, AppLocalizations().remindersNotificationChannelDescription); + expect(details.android?.channelId, NotificationUtil.notificationChannelReminders); + expect(details.android?.channelName, AppLocalizations().remindersNotificationChannelName); + expect(details.android?.channelDescription, AppLocalizations().remindersNotificationChannelDescription); verify(analytics.logEvent(AnalyticsEventConstants.REMINDER_EVENT_CREATE)); }); @@ -164,19 +173,24 @@ void main() { await NotificationUtil().scheduleReminder(AppLocalizations(), 'title', 'body', reminder); - final NotificationDetails details = verify(plugin.schedule( + tz.initializeTimeZones(); + var d = reminder.date!.toUtc(); + var date = tz.TZDateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second); + + final NotificationDetails details = verify(plugin.zonedSchedule( reminder.id, 'title', 'body', - reminder.date, + date, captureAny, payload: json.encode(serialize(expectedPayload)), + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, )).captured.first; expect(details.iOS, isNull); - expect(details.android.channelId, NotificationUtil.notificationChannelReminders); - expect(details.android.channelName, AppLocalizations().remindersNotificationChannelName); - expect(details.android.channelDescription, AppLocalizations().remindersNotificationChannelDescription); + expect(details.android?.channelId, NotificationUtil.notificationChannelReminders); + expect(details.android?.channelName, AppLocalizations().remindersNotificationChannelName); + expect(details.android?.channelDescription, AppLocalizations().remindersNotificationChannelDescription); verify(analytics.logEvent(AnalyticsEventConstants.REMINDER_ASSIGNMENT_CREATE)); }); diff --git a/apps/flutter_parent/test/utils/old_app_migrations_test.dart b/apps/flutter_parent/test/utils/old_app_migrations_test.dart index f7c310d3dd..a10c69927b 100644 --- a/apps/flutter_parent/test/utils/old_app_migrations_test.dart +++ b/apps/flutter_parent/test/utils/old_app_migrations_test.dart @@ -53,7 +53,7 @@ void main() { List serializedLogins = logins.map((it) => json.encode(serialize(it))).toList(); - String calledMethod; + String? calledMethod; OldAppMigration.channel.setMockMethodCallHandler((MethodCall methodCall) async { calledMethod = methodCall.method; return serializedLogins; @@ -85,7 +85,7 @@ void main() { test('calls platform channel and sets checked value', () async { bool channelReturnValue = true; - String calledMethod; + String? calledMethod; OldAppMigration.channel.setMockMethodCallHandler((MethodCall methodCall) async { calledMethod = methodCall.method; diff --git a/apps/flutter_parent/test/utils/platform_config.dart b/apps/flutter_parent/test/utils/platform_config.dart index fc685958cf..88ee3bfdc1 100644 --- a/apps/flutter_parent/test/utils/platform_config.dart +++ b/apps/flutter_parent/test/utils/platform_config.dart @@ -19,14 +19,15 @@ import 'package:flutter_parent/models/login.dart'; class PlatformConfig { final bool initDeviceInfo; - final Login initLoggedInUser; + final Login? initLoggedInUser; final bool initPackageInfo; - final RemoteConfig initRemoteConfig; + final bool initPathProvider; + final FirebaseRemoteConfig? initRemoteConfig; final bool initWebview; - final Map _mockApiPrefs; + final Map? _mockApiPrefs; - final Map _mockPrefs; + final Map? _mockPrefs; static const _testPrefix = 'flutter.'; @@ -43,19 +44,21 @@ class PlatformConfig { this.initDeviceInfo = true, this.initLoggedInUser = null, this.initPackageInfo = true, + this.initPathProvider = true, this.initRemoteConfig = null, this.initWebview = false, - Map mockApiPrefs = const {}, - Map mockPrefs = null, + + Map? mockApiPrefs = const {}, + Map? mockPrefs = null, }) : this._mockApiPrefs = mockApiPrefs, this._mockPrefs = mockPrefs; /// SharedPreferences requires that test configurations use 'flutter.' at the beginning of keys in the map - Map get mockApiPrefs => _safeMap(_mockApiPrefs); + Map? get mockApiPrefs => _safeMap(_mockApiPrefs); - Map get mockPrefs => _safeMap(_mockPrefs); + Map? get mockPrefs => _safeMap(_mockPrefs); - Map _safeMap(Map map) => map?.map((k, v) => MapEntry(_testKey(k), v)); + Map? _safeMap(Map? map) => map?.map((k, v) => MapEntry(_testKey(k), v)); String _testKey(String key) { return key.startsWith(_testPrefix) ? key : '$_testPrefix$key'; diff --git a/apps/flutter_parent/test/utils/qr_utils_test.dart b/apps/flutter_parent/test/utils/qr_utils_test.dart index 9fe58f4115..fc73edc7de 100644 --- a/apps/flutter_parent/test/utils/qr_utils_test.dart +++ b/apps/flutter_parent/test/utils/qr_utils_test.dart @@ -23,6 +23,7 @@ import 'package:test/test.dart'; import 'test_app.dart'; import 'test_helpers/mock_helpers.dart'; +import 'test_helpers/mock_helpers.mocks.dart'; void main() { String _getQRUrl( @@ -102,7 +103,7 @@ void main() { }); group('scanPairingCode', () { - BarcodeScanVeneer barcodeScanner = MockBarcodeScanner(); + MockBarcodeScanVeneer barcodeScanner = MockBarcodeScanVeneer(); setupTestLocator((locator) => locator.registerLazySingleton(() => barcodeScanner)); setUp(() { @@ -115,8 +116,8 @@ void main() { var result = await QRUtils.scanPairingCode(); expect(result, isA()); expect((result as QRPairingInfo).domain, 'test.instructure.com'); - expect((result as QRPairingInfo).code, 'aBc123'); - expect((result as QRPairingInfo).accountId, '1234'); + expect((result).code, 'aBc123'); + expect((result).accountId, '1234'); }); test('scanPairingCode returns canceled result if scan was canceled', () async { diff --git a/apps/flutter_parent/test/utils/style_slicer_test.dart b/apps/flutter_parent/test/utils/style_slicer_test.dart index 788eb56395..dbd9d4b2c1 100644 --- a/apps/flutter_parent/test/utils/style_slicer_test.dart +++ b/apps/flutter_parent/test/utils/style_slicer_test.dart @@ -20,7 +20,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('returns basic span if slicer list is null', () { final String source = 'User Name'; - final List slicers = null; + final List? slicers = null; TextSpan actual = StyleSlicer.apply(source, slicers); @@ -50,7 +50,7 @@ void main() { test('ignores null slicers', () { final String source = 'User Name'; - final List slicers = [null]; + final List slicers = []; TextSpan actual = StyleSlicer.apply(source, slicers); @@ -69,7 +69,7 @@ void main() { }); test('returns empty span if source is null', () { - final String source = null; + final String? source = null; final List slicers = []; TextSpan actual = StyleSlicer.apply(source, slicers); @@ -94,13 +94,13 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 3); + expect(actual.children?.length, 3); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); - expect(spans[2].text, ' name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); + expect(spans?[2].text, ' name'); }); test('returns correct span for single beginning match', () { @@ -109,12 +109,12 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 2); + expect(actual.children?.length, 2); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, '(pro/noun)'); - expect(spans[0].style.fontStyle, FontStyle.italic); - expect(spans[1].text, ' user name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, '(pro/noun)'); + expect(spans?[0].style?.fontStyle, FontStyle.italic); + expect(spans?[1].text, ' user name'); }); test('returns correct span for single end match', () { @@ -123,12 +123,12 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 2); + expect(actual.children?.length, 2); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user name '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user name '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); }); test('returns correct span for multiple middle match', () { @@ -137,16 +137,16 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 5); + expect(actual.children?.length, 5); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); - expect(spans[2].text, ' middle '); - expect(spans[3].text, '(pro/noun)'); - expect(spans[3].style.fontStyle, FontStyle.italic); - expect(spans[4].text, ' name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); + expect(spans?[2].text, ' middle '); + expect(spans?[3].text, '(pro/noun)'); + expect(spans?[3].style?.fontStyle, FontStyle.italic); + expect(spans?[4].text, ' name'); }); test('returns correct span for duplicate pronouns', () { @@ -155,16 +155,16 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 5); + expect(actual.children?.length, 5); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); - expect(spans[2].text, ' middle '); - expect(spans[3].text, '(pro/noun)'); - expect(spans[3].style.fontStyle, FontStyle.italic); - expect(spans[4].text, ' name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); + expect(spans?[2].text, ' middle '); + expect(spans?[3].text, '(pro/noun)'); + expect(spans?[3].style?.fontStyle, FontStyle.italic); + expect(spans?[4].text, ' name'); }); test('returns correct span for multiple beginning match', () { @@ -173,15 +173,15 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 4); + expect(actual.children?.length, 4); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, '(pro/noun)'); - expect(spans[0].style.fontStyle, FontStyle.italic); - expect(spans[1].text, ' user middle '); - expect(spans[2].text, '(pro/noun)'); - expect(spans[2].style.fontStyle, FontStyle.italic); - expect(spans[3].text, ' name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, '(pro/noun)'); + expect(spans?[0].style?.fontStyle, FontStyle.italic); + expect(spans?[1].text, ' user middle '); + expect(spans?[2].text, '(pro/noun)'); + expect(spans?[2].style?.fontStyle, FontStyle.italic); + expect(spans?[3].text, ' name'); }); test('returns correct span for multiple end match', () { @@ -190,15 +190,15 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 4); + expect(actual.children?.length, 4); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); - expect(spans[2].text, ' middle name '); - expect(spans[3].text, '(pro/noun)'); - expect(spans[3].style.fontStyle, FontStyle.italic); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); + expect(spans?[2].text, ' middle name '); + expect(spans?[3].text, '(pro/noun)'); + expect(spans?[3].style?.fontStyle, FontStyle.italic); }); test('returns correct span for multiple pronouns', () { @@ -207,16 +207,16 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 5); + expect(actual.children?.length, 5); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); - expect(spans[2].text, ' middle '); - expect(spans[3].text, '(noun/pro)'); - expect(spans[3].style.fontStyle, FontStyle.italic); - expect(spans[4].text, ' name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); + expect(spans?[2].text, ' middle '); + expect(spans?[3].text, '(noun/pro)'); + expect(spans?[3].style?.fontStyle, FontStyle.italic); + expect(spans?[4].text, ' name'); }); test('returns correct span for adjacent pronouns', () { @@ -225,15 +225,15 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 4); + expect(actual.children?.length, 4); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'user '); - expect(spans[1].text, '(pro/noun)'); - expect(spans[1].style.fontStyle, FontStyle.italic); - expect(spans[2].text, '(noun/pro)'); - expect(spans[2].style.fontStyle, FontStyle.italic); - expect(spans[3].text, ' name'); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'user '); + expect(spans?[1].text, '(pro/noun)'); + expect(spans?[1].style?.fontStyle, FontStyle.italic); + expect(spans?[2].text, '(noun/pro)'); + expect(spans?[2].style?.fontStyle, FontStyle.italic); + expect(spans?[3].text, ' name'); }); test('returns correct span for overlapping styles', () { @@ -245,23 +245,23 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 5); + expect(actual.children?.length, 5); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'Normal '); - expect(spans[0].style, TextStyle()); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'Normal '); + expect(spans?[0].style, TextStyle()); - expect(spans[1].text, 'Bold '); - expect(spans[1].style, TextStyle(fontWeight: FontWeight.bold)); + expect(spans?[1].text, 'Bold '); + expect(spans?[1].style, TextStyle(fontWeight: FontWeight.bold)); - expect(spans[2].text, 'Bold-Small'); - expect(spans[2].style, TextStyle(fontWeight: FontWeight.bold, fontSize: 8)); + expect(spans?[2].text, 'Bold-Small'); + expect(spans?[2].style, TextStyle(fontWeight: FontWeight.bold, fontSize: 8)); - expect(spans[3].text, ' Small'); - expect(spans[3].style, TextStyle(fontSize: 8)); + expect(spans?[3].text, ' Small'); + expect(spans?[3].style, TextStyle(fontSize: 8)); - expect(spans[4].text, ' Normal'); - expect(spans[4].style, TextStyle()); + expect(spans?[4].text, ' Normal'); + expect(spans?[4].style, TextStyle()); }); test('uses last-declared gesture recognizer', () { @@ -276,23 +276,23 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 5); + expect(actual.children?.length, 5); - var spans = actual.children.map((it) => it as TextSpan).toList(); - expect(spans[0].text, 'Click '); - expect(spans[0].recognizer, isNull); + var spans = actual.children?.map((it) => it as TextSpan).toList(); + expect(spans?[0].text, 'Click '); + expect(spans?[0].recognizer, isNull); - expect(spans[1].text, 'here '); - expect(spans[1].recognizer, recognizer1); + expect(spans?[1].text, 'here '); + expect(spans?[1].recognizer, recognizer1); - expect(spans[2].text, 'or here'); - expect(spans[2].recognizer, recognizer2); + expect(spans?[2].text, 'or here'); + expect(spans?[2].recognizer, recognizer2); - expect(spans[3].text, ' to'); - expect(spans[3].recognizer, recognizer1); + expect(spans?[3].text, ' to'); + expect(spans?[3].recognizer, recognizer1); - expect(spans[4].text, ' proceed'); - expect(spans[4].recognizer, null); + expect(spans?[4].text, ' proceed'); + expect(spans?[4].recognizer, null); }); test('PatternSlice finds all matches by default', () { @@ -301,7 +301,7 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 100); + expect(actual.children?.length, 100); }); test('PatternSlice limits matches if maxMatches is specified', () { @@ -310,6 +310,6 @@ void main() { TextSpan actual = StyleSlicer.apply(source, slicers); - expect(actual.children.length, 2); + expect(actual.children?.length, 2); }); } diff --git a/apps/flutter_parent/test/utils/test_app.dart b/apps/flutter_parent/test/utils/test_app.dart index f7bf0f981f..f477beeab6 100644 --- a/apps/flutter_parent/test/utils/test_app.dart +++ b/apps/flutter_parent/test/utils/test_app.dart @@ -36,6 +36,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'platform_config.dart'; import 'test_helpers/mock_helpers.dart'; +import 'test_helpers/mock_helpers.mocks.dart'; class TestApp extends StatefulWidget { TestApp( @@ -54,7 +55,7 @@ class TestApp extends StatefulWidget { final List navigatorObservers; final bool darkMode; final bool highContrast; - final Locale locale; + final Locale? locale; @override _TestAppState createState() => _TestAppState(); @@ -83,14 +84,14 @@ class TestApp extends StatefulWidget { static showWidgetFromTap( WidgetTester tester, Future tapCallback(BuildContext context), { - Locale locale, + Locale? locale, PlatformConfig config = const PlatformConfig(), - Future configBlock(), + Future configBlock()?, }) async { await tester.pumpWidget(TestApp( Builder(builder: (context) { - return RaisedButton( - color: Colors.black, + return ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), child: Text('tap me', style: TextStyle(color: Colors.white)), onPressed: () => tapCallback(context), ); @@ -104,13 +105,13 @@ class TestApp extends StatefulWidget { if (configBlock != null) await configBlock(); // Tap the button to trigger the onPressed - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); } } class _TestAppState extends State { - Locale _locale; + Locale? _locale; rebuild(locale) { setState(() => _locale = locale); @@ -132,7 +133,9 @@ class _TestAppState extends State { builder: (context, themeData) => MaterialApp( title: 'Canvas Parent', locale: _locale, - builder: (context, child) => MasqueradeUI(navKey: TestApp.navigatorKey, child: child), + builder: (context, child) => + MasqueradeUI(navKey: TestApp.navigatorKey, + child: child ?? Container()), navigatorKey: TestApp.navigatorKey, navigatorObservers: widget.navigatorObservers, localizationsDelegates: const [ @@ -140,6 +143,7 @@ class _TestAppState extends State { // Material components use these delegate to provide default localization GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, ], supportedLocales: AppLocalizations.delegate.supportedLocales, localeResolutionCallback: _localeCallback(), @@ -154,7 +158,7 @@ class _TestAppState extends State { // Get notified when there's a new system locale so we can rebuild the app with the new language LocaleResolutionCallback _localeCallback() => (locale, supportedLocales) { const fallback = Locale('en', ''); - Locale resolvedLocale = + Locale? resolvedLocale = AppLocalizations.delegate.resolution(fallback: fallback, matchCountry: false)(locale, supportedLocales); // Update the state if the locale changed @@ -168,7 +172,7 @@ class _TestAppState extends State { }; } -void setupTestLocator(config(GetIt locator)) async { +Future setupTestLocator(config(GetIt locator)) async { final locator = GetIt.instance; await locator.reset(); locator.allowReassignment = true; // Allows reassignment by the config block @@ -223,20 +227,22 @@ Future setupPlatformChannels({PlatformConfig config = const PlatformConfig if (config.initDeviceInfo) _initPlatformDeviceInfo(); - Future apiPrefsInitFuture; + if (config.initPathProvider) _initPathProvider(); + + Future? apiPrefsInitFuture; if (config.mockApiPrefs != null) { ApiPrefs.clean(); EncryptedSharedPreferences.setMockInitialValues( - config.mockApiPrefs..putIfAbsent(ApiPrefs.KEY_HAS_MIGRATED_TO_ENCRYPTED_PREFS, () => true)); + config.mockApiPrefs!..putIfAbsent(ApiPrefs.KEY_HAS_MIGRATED_TO_ENCRYPTED_PREFS, () => true)); apiPrefsInitFuture = ApiPrefs.init(); } - Future remoteConfigInitFuture; + Future? remoteConfigInitFuture; if (config.initRemoteConfig != null || config.mockPrefs != null) { SharedPreferences.setMockInitialValues(config.mockPrefs ?? {}); if (config.initRemoteConfig != null) { RemoteConfigUtils.clean(); - remoteConfigInitFuture = RemoteConfigUtils.initializeExplicit(config.initRemoteConfig); + remoteConfigInitFuture = RemoteConfigUtils.initializeExplicit(config.initRemoteConfig!); } } @@ -248,8 +254,8 @@ Future setupPlatformChannels({PlatformConfig config = const PlatformConfig ]); if (config.initLoggedInUser != null) { - ApiPrefs.addLogin(config.initLoggedInUser); - ApiPrefs.switchLogins(config.initLoggedInUser); + ApiPrefs.addLogin(config.initLoggedInUser!); + ApiPrefs.switchLogins(config.initLoggedInUser!); } } @@ -261,7 +267,7 @@ Future setupPlatformChannels({PlatformConfig config = const PlatformConfig /// https://github.com/flutter/plugins/blob/3b71d6e9a4456505f0b079074fcbc9ba9f8e0e15/packages/webview_flutter/test/webview_flutter_test.dart void _initPlatformWebView() { const MethodChannel('plugins.flutter.io/cookie_manager', const StandardMethodCodec()) - .setMockMethodCallHandler((_) => Future.sync(() => null)); + .setMockMethodCallHandler((_) => Future.sync(() => false)); // Intercept when a web view is getting created so we can set up the platform channel SystemChannels.platform_views.setMockMethodCallHandler((call) { @@ -279,7 +285,7 @@ void _initPlatformWebView() { /// Mocks the platform channel used by the package_info plugin void _initPackageInfo() { - const MethodChannel('plugins.flutter.io/package_info').setMockMethodCallHandler((MethodCall methodCall) async { + const MethodChannel('dev.fluttercommunity.plus/package_info').setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'getAll') { return { 'appName': 'Canvas', @@ -294,7 +300,7 @@ void _initPackageInfo() { /// Mocks the platform channel used by the device_info plugin void _initPlatformDeviceInfo() { - const MethodChannel('plugins.flutter.io/device_info').setMockMethodCallHandler((MethodCall methodCall) async { + const MethodChannel('dev.fluttercommunity.plus/device_info').setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'getAndroidDeviceInfo') { return { 'version': { @@ -325,8 +331,64 @@ void _initPlatformDeviceInfo() { 'type': 'take-types', 'isPhysicalDevice': false, 'androidId': 'fake-androidId', + 'displayMetrics': { + 'widthPx': 100.0, + 'heightPx': 100.0, + 'xDpi': 100.0, + 'yDpi': 100.0, + }, + 'serialNumber': 'fake-serialNumber', + }; + } + if (methodCall.method == 'getDeviceInfo') { + return { + 'version': { + 'baseOS': 'fake-baseOD', + 'codename': 'fake-codename', + 'incremental': 'fake-incremental', + 'previewSdkInt': 9001, + 'release': 'FakeOS 9000', + 'sdkInt': 9000, + 'securityPatch': 'fake-securityPatch', + }, + 'board': 'fake-board', + 'bootloader': 'fake-bootloader', + 'brand': 'Canvas', + 'device': 'fake-device', + 'display': 'fake-display', + 'fingerprint': 'fake-fingerprint', + 'hardware': 'fake-hardware', + 'host': 'fake-host', + 'id': 'fake-id', + 'manufacturer': 'Instructure', + 'model': 'Canvas Phone', + 'product': 'fake-product', + 'supported32BitAbis': [], + 'supported64BitAbis': [], + 'supportedAbis': [], + 'tags': 'fake-tags', + 'type': 'take-types', + 'isPhysicalDevice': false, + 'androidId': 'fake-androidId', + 'displayMetrics': { + 'widthPx': 100.0, + 'heightPx': 100.0, + 'xDpi': 100.0, + 'yDpi': 100.0, + }, + 'serialNumber': 'fake-serialNumber', }; } return null; }); } + +/// Mocks the platform channel used by the path_provider plugin +void _initPathProvider() { + const MethodChannel('plugins.flutter.io/path_provider').setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'getApplicationCacheDirectory') { + return "fake-path"; + } + return null; + }); +} \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart index c9afd5b328..99e7339c43 100644 --- a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart +++ b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart @@ -19,52 +19,181 @@ // settings will correspond the specified values. import 'dart:io'; +import 'package:barcode_scan2/platform_wrapper.dart'; import 'package:dio/dio.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_parent/network/api/accounts_api.dart'; import 'package:flutter_parent/network/api/alert_api.dart'; +import 'package:flutter_parent/network/api/announcement_api.dart'; import 'package:flutter_parent/network/api/assignment_api.dart'; import 'package:flutter_parent/network/api/auth_api.dart'; import 'package:flutter_parent/network/api/calendar_events_api.dart'; import 'package:flutter_parent/network/api/course_api.dart'; import 'package:flutter_parent/network/api/enrollments_api.dart'; import 'package:flutter_parent/network/api/error_report_api.dart'; +import 'package:flutter_parent/network/api/features_api.dart'; +import 'package:flutter_parent/network/api/file_api.dart'; +import 'package:flutter_parent/network/api/help_links_api.dart'; import 'package:flutter_parent/network/api/inbox_api.dart'; import 'package:flutter_parent/network/api/oauth_api.dart'; import 'package:flutter_parent/network/api/page_api.dart'; +import 'package:flutter_parent/network/api/planner_api.dart'; +import 'package:flutter_parent/network/api/user_api.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; +import 'package:flutter_parent/network/utils/authentication_interceptor.dart'; +import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/screens/account_creation/account_creation_interactor.dart'; +import 'package:flutter_parent/screens/alert_thresholds/alert_thresholds_interactor.dart'; +import 'package:flutter_parent/screens/alerts/alerts_interactor.dart'; +import 'package:flutter_parent/screens/announcements/announcement_details_interactor.dart'; import 'package:flutter_parent/screens/assignments/assignment_details_interactor.dart'; +import 'package:flutter_parent/screens/aup/acceptable_use_policy_interactor.dart'; import 'package:flutter_parent/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_interactor.dart'; import 'package:flutter_parent/screens/courses/courses_interactor.dart'; import 'package:flutter_parent/screens/courses/details/course_details_interactor.dart'; import 'package:flutter_parent/screens/courses/details/course_details_model.dart'; import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shell_interactor.dart'; import 'package:flutter_parent/screens/dashboard/alert_notifier.dart'; +import 'package:flutter_parent/screens/dashboard/dashboard_interactor.dart'; +import 'package:flutter_parent/screens/dashboard/inbox_notifier.dart'; +import 'package:flutter_parent/screens/dashboard/selected_student_notifier.dart'; +import 'package:flutter_parent/screens/domain_search/domain_search_interactor.dart'; import 'package:flutter_parent/screens/events/event_details_interactor.dart'; +import 'package:flutter_parent/screens/help/help_screen_interactor.dart'; +import 'package:flutter_parent/screens/inbox/attachment_utils/attachment_handler.dart'; +import 'package:flutter_parent/screens/inbox/attachment_utils/attachment_picker_interactor.dart'; +import 'package:flutter_parent/screens/inbox/conversation_details/conversation_details_interactor.dart'; +import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_interactor.dart'; import 'package:flutter_parent/screens/inbox/create_conversation/create_conversation_interactor.dart'; +import 'package:flutter_parent/screens/inbox/reply/conversation_reply_interactor.dart'; +import 'package:flutter_parent/screens/manage_students/manage_students_interactor.dart'; +import 'package:flutter_parent/screens/manage_students/student_color_picker_interactor.dart'; +import 'package:flutter_parent/screens/masquerade/masquerade_screen_interactor.dart'; import 'package:flutter_parent/screens/pairing/pairing_interactor.dart'; import 'package:flutter_parent/screens/pairing/pairing_util.dart'; +import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen_interactor.dart'; +import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart'; +import 'package:flutter_parent/screens/settings/settings_interactor.dart'; +import 'package:flutter_parent/screens/splash/splash_screen_interactor.dart'; import 'package:flutter_parent/screens/web_login/web_login_interactor.dart'; import 'package:flutter_parent/utils/common_widgets/error_report/error_report_interactor.dart'; +import 'package:flutter_parent/utils/common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart'; +import 'package:flutter_parent/utils/common_widgets/view_attachment/view_attachment_interactor.dart'; +import 'package:flutter_parent/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer_interactor.dart'; import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart'; import 'package:flutter_parent/utils/db/calendar_filter_db.dart'; import 'package:flutter_parent/utils/db/reminder_db.dart'; import 'package:flutter_parent/utils/db/user_colors_db.dart'; import 'package:flutter_parent/utils/notification_util.dart'; +import 'package:flutter_parent/utils/old_app_migration.dart'; +import 'package:flutter_parent/utils/permission_handler.dart'; import 'package:flutter_parent/utils/quick_nav.dart'; import 'package:flutter_parent/utils/url_launcher.dart'; import 'package:flutter_parent/utils/veneers/android_intent_veneer.dart'; import 'package:flutter_parent/utils/veneers/barcode_scan_veneer.dart'; +import 'package:flutter_parent/utils/veneers/flutter_downloader_veneer.dart'; import 'package:flutter_parent/utils/veneers/flutter_snackbar_veneer.dart'; +import 'package:flutter_parent/utils/veneers/path_provider_veneer.dart'; +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sqflite/sqflite.dart'; - -MockRemoteConfig setupMockRemoteConfig({Map valueSettings = null}) { - final mockRemoteConfig = MockRemoteConfig(); +import 'package:video_player/video_player.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +import 'mock_helpers.mocks.dart'; + +MockFirebaseRemoteConfig setupMockRemoteConfig({Map? valueSettings = null}) { + final mockRemoteConfig = MockFirebaseRemoteConfig(); when(mockRemoteConfig.fetch()).thenAnswer((_) => Future.value()); when(mockRemoteConfig.activate()) .thenAnswer((_) => Future.value(valueSettings != null)); @@ -79,100 +208,4 @@ MockRemoteConfig setupMockRemoteConfig({Map valueSettings = null } return mockRemoteConfig; -} - -class MockAccountsApi extends Mock implements AccountsApi {} - -class MockAccountCreationInteractor extends Mock implements AccountCreationInteractor {} - -class MockAnalytics extends Mock implements Analytics {} - -class MockAndroidIntentVeneer extends Mock implements AndroidIntentVeneer {} - -class MockAlertsApi extends Mock implements AlertsApi {} - -class MockAlertCountNotifier extends Mock implements AlertCountNotifier {} - -class MockAssignmentApi extends Mock implements AssignmentApi {} - -class MockAssignmentDetailsInteractor extends Mock implements AssignmentDetailsInteractor {} - -class MockAuthApi extends Mock implements AuthApi {} - -class MockBarcodeScanner extends Mock implements BarcodeScanVeneer {} - -class MockCalendarApi extends Mock implements CalendarEventsApi {} - -class MockCalendarFilterDb extends Mock implements CalendarFilterDb {} - -class MockCalendarFilterListInteractor extends Mock implements CalendarFilterListInteractor {} - -class MockCourseApi extends Mock implements CourseApi {} - -class MockCourseDetailsInteractor extends Mock implements CourseDetailsInteractor {} - -class MockCoursesInteractor extends Mock implements CoursesInteractor {} - -class MockCourseModel extends Mock implements CourseDetailsModel {} - -class MockCourseRoutingShellInteractor extends Mock implements CourseRoutingShellInteractor {} - -class MockCreateConversationInteractor extends Mock implements CreateConversationInteractor {} - -class MockDatabase extends Mock implements Database {} - -class MockDio extends Mock implements Dio {} - -class MockEnrollmentsApi extends Mock implements EnrollmentsApi {} - -class MockErrorReportApi extends Mock implements ErrorReportApi {} - -class MockErrorReportInteractor extends Mock implements ErrorReportInteractor {} - -class MockEventDetailsInteractor extends Mock implements EventDetailsInteractor {} - -class MockFirebase extends Mock implements FirebaseCrashlytics {} - -class MockHttpClient extends Mock implements HttpClient {} - -class MockHttpClientRequest extends Mock implements HttpClientRequest {} - -class MockHttpClientResponse extends Mock implements HttpClientResponse {} - -class MockHttpHeaders extends Mock implements HttpHeaders {} - -class MockInboxApi extends Mock implements InboxApi {} - -class MockNav extends Mock implements QuickNav {} - -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - -class MockNotificationUtil extends Mock implements NotificationUtil {} - -class MockOAuthApi extends Mock implements OAuthApi {} - -class MockPairingInteractor extends Mock implements PairingInteractor {} - -class MockPageApi extends Mock implements PageApi {} - -class MockPlugin extends Mock implements FlutterLocalNotificationsPlugin {} - -class MockPairingUtil extends Mock implements PairingUtil {} - -class MockQuickNav extends Mock implements QuickNav {} - -class MockReminderDb extends Mock implements ReminderDb {} - -class MockRemoteConfig extends Mock implements RemoteConfig {} - -class MockSnackbar extends Mock implements FlutterSnackbarVeneer {} - -class MockStudentAddedNotifier extends Mock implements StudentAddedNotifier {} - -class MockUrlLauncher extends Mock implements UrlLauncher {} - -class MockUserColorsDb extends Mock implements UserColorsDb {} - -class MockWebLoginInteractor extends Mock implements WebLoginInteractor {} - -class MockWebContentInteractor extends Mock implements WebContentInteractor {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart new file mode 100644 index 0000000000..326113c085 --- /dev/null +++ b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart @@ -0,0 +1,9042 @@ +// Mocks generated by Mockito 5.4.2 from annotations +// in flutter_parent/test/utils/test_helpers/mock_helpers.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i8; +import 'dart:convert' as _i12; +import 'dart:io' as _i10; +import 'dart:ui' as _i35; + +import 'package:android_intent_plus/android_intent.dart' as _i30; +import 'package:barcode_scan2/barcode_scan2.dart' as _i4; +import 'package:built_collection/built_collection.dart' as _i108; +import 'package:dio/dio.dart' as _i2; +import 'package:dio/src/dio_mixin.dart' as _i22; +import 'package:firebase_core/firebase_core.dart' as _i9; +import 'package:firebase_crashlytics/firebase_crashlytics.dart' as _i72; +import 'package:firebase_remote_config/firebase_remote_config.dart' as _i86; +import 'package:firebase_remote_config_platform_interface/firebase_remote_config_platform_interface.dart' + as _i14; +import 'package:fluro/fluro.dart' as _i84; +import 'package:flutter/foundation.dart' as _i11; +import 'package:flutter/material.dart' as _i17; +import 'package:flutter/services.dart' as _i15; +import 'package:flutter_downloader/flutter_downloader.dart' as _i133; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' + as _i80; +import 'package:flutter_parent/l10n/app_localizations.dart' as _i41; +import 'package:flutter_parent/models/account_notification.dart' as _i102; +import 'package:flutter_parent/models/account_permissions.dart' as _i26; +import 'package:flutter_parent/models/alert.dart' as _i32; +import 'package:flutter_parent/models/alert_threshold.dart' as _i34; +import 'package:flutter_parent/models/announcement.dart' as _i101; +import 'package:flutter_parent/models/assignment.dart' as _i37; +import 'package:flutter_parent/models/assignment_group.dart' as _i38; +import 'package:flutter_parent/models/attachment.dart' as _i99; +import 'package:flutter_parent/models/authenticated_url.dart' as _i77; +import 'package:flutter_parent/models/calendar_filter.dart' as _i49; +import 'package:flutter_parent/models/canvas_page.dart' as _i59; +import 'package:flutter_parent/models/canvas_token.dart' as _i43; +import 'package:flutter_parent/models/color_change_response.dart' as _i123; +import 'package:flutter_parent/models/conversation.dart' as _i65; +import 'package:flutter_parent/models/course.dart' as _i51; +import 'package:flutter_parent/models/course_permissions.dart' as _i56; +import 'package:flutter_parent/models/course_settings.dart' as _i55; +import 'package:flutter_parent/models/course_tab.dart' as _i54; +import 'package:flutter_parent/models/enrollment.dart' as _i58; +import 'package:flutter_parent/models/feature_flags.dart' as _i127; +import 'package:flutter_parent/models/grading_period_response.dart' as _i53; +import 'package:flutter_parent/models/help_link.dart' as _i107; +import 'package:flutter_parent/models/help_links.dart' as _i139; +import 'package:flutter_parent/models/message.dart' as _i113; +import 'package:flutter_parent/models/mobile_verify_result.dart' as _i44; +import 'package:flutter_parent/models/planner_item.dart' as _i105; +import 'package:flutter_parent/models/recipient.dart' as _i74; +import 'package:flutter_parent/models/reminder.dart' as _i40; +import 'package:flutter_parent/models/schedule_item.dart' as _i47; +import 'package:flutter_parent/models/school_domain.dart' as _i24; +import 'package:flutter_parent/models/terms_of_service.dart' as _i25; +import 'package:flutter_parent/models/unread_count.dart' as _i33; +import 'package:flutter_parent/models/user.dart' as _i61; +import 'package:flutter_parent/models/user_color.dart' as _i90; +import 'package:flutter_parent/models/user_colors.dart' as _i91; +import 'package:flutter_parent/network/api/accounts_api.dart' as _i23; +import 'package:flutter_parent/network/api/alert_api.dart' as _i31; +import 'package:flutter_parent/network/api/announcement_api.dart' as _i100; +import 'package:flutter_parent/network/api/assignment_api.dart' as _i36; +import 'package:flutter_parent/network/api/auth_api.dart' as _i42; +import 'package:flutter_parent/network/api/calendar_events_api.dart' as _i46; +import 'package:flutter_parent/network/api/course_api.dart' as _i52; +import 'package:flutter_parent/network/api/enrollments_api.dart' as _i68; +import 'package:flutter_parent/network/api/error_report_api.dart' as _i69; +import 'package:flutter_parent/network/api/features_api.dart' as _i126; +import 'package:flutter_parent/network/api/file_api.dart' as _i109; +import 'package:flutter_parent/network/api/help_links_api.dart' as _i138; +import 'package:flutter_parent/network/api/inbox_api.dart' as _i73; +import 'package:flutter_parent/network/api/oauth_api.dart' as _i76; +import 'package:flutter_parent/network/api/page_api.dart' as _i79; +import 'package:flutter_parent/network/api/planner_api.dart' as _i104; +import 'package:flutter_parent/network/api/user_api.dart' as _i122; +import 'package:flutter_parent/network/utils/analytics.dart' as _i28; +import 'package:flutter_parent/network/utils/authentication_interceptor.dart' + as _i135; +import 'package:flutter_parent/network/utils/paged_list.dart' as _i39; +import 'package:flutter_parent/screens/account_creation/account_creation_interactor.dart' + as _i27; +import 'package:flutter_parent/screens/alert_thresholds/alert_thresholds_interactor.dart' + as _i94; +import 'package:flutter_parent/screens/alerts/alerts_interactor.dart' as _i95; +import 'package:flutter_parent/screens/announcements/announcement_details_interactor.dart' + as _i96; +import 'package:flutter_parent/screens/announcements/announcement_details_screen.dart' + as _i98; +import 'package:flutter_parent/screens/announcements/announcement_view_state.dart' + as _i97; +import 'package:flutter_parent/screens/assignments/assignment_details_interactor.dart' + as _i3; +import 'package:flutter_parent/screens/aup/acceptable_use_policy_interactor.dart' + as _i103; +import 'package:flutter_parent/screens/calendar/calendar_widget/calendar_filter_screen/calendar_filter_list_interactor.dart' + as _i50; +import 'package:flutter_parent/screens/courses/courses_interactor.dart' as _i60; +import 'package:flutter_parent/screens/courses/details/course_details_interactor.dart' + as _i57; +import 'package:flutter_parent/screens/courses/details/course_details_model.dart' + as _i6; +import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shell_interactor.dart' + as _i63; +import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shell_screen.dart' + as _i64; +import 'package:flutter_parent/screens/dashboard/alert_notifier.dart' as _i19; +import 'package:flutter_parent/screens/dashboard/dashboard_interactor.dart' + as _i118; +import 'package:flutter_parent/screens/dashboard/inbox_notifier.dart' as _i18; +import 'package:flutter_parent/screens/domain_search/domain_search_interactor.dart' + as _i117; +import 'package:flutter_parent/screens/events/event_details_interactor.dart' + as _i71; +import 'package:flutter_parent/screens/help/help_screen_interactor.dart' + as _i106; +import 'package:flutter_parent/screens/inbox/attachment_utils/attachment_handler.dart' + as _i66; +import 'package:flutter_parent/screens/inbox/attachment_utils/attachment_picker_interactor.dart' + as _i136; +import 'package:flutter_parent/screens/inbox/conversation_details/conversation_details_interactor.dart' + as _i112; +import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_interactor.dart' + as _i114; +import 'package:flutter_parent/screens/inbox/create_conversation/create_conversation_interactor.dart' + as _i7; +import 'package:flutter_parent/screens/inbox/reply/conversation_reply_interactor.dart' + as _i116; +import 'package:flutter_parent/screens/manage_students/manage_students_interactor.dart' + as _i120; +import 'package:flutter_parent/screens/manage_students/student_color_picker_interactor.dart' + as _i121; +import 'package:flutter_parent/screens/masquerade/masquerade_screen_interactor.dart' + as _i124; +import 'package:flutter_parent/screens/pairing/pairing_interactor.dart' as _i78; +import 'package:flutter_parent/screens/pairing/pairing_util.dart' as _i82; +import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen_interactor.dart' + as _i20; +import 'package:flutter_parent/screens/remote_config/remote_config_interactor.dart' + as _i140; +import 'package:flutter_parent/screens/settings/settings_interactor.dart' + as _i125; +import 'package:flutter_parent/screens/splash/splash_screen_interactor.dart' + as _i128; +import 'package:flutter_parent/screens/web_login/web_login_interactor.dart' + as _i92; +import 'package:flutter_parent/utils/base_model.dart' as _i62; +import 'package:flutter_parent/utils/common_widgets/error_report/error_report_interactor.dart' + as _i70; +import 'package:flutter_parent/utils/common_widgets/view_attachment/fetcher/attachment_fetcher_interactor.dart' + as _i129; +import 'package:flutter_parent/utils/common_widgets/view_attachment/view_attachment_interactor.dart' + as _i134; +import 'package:flutter_parent/utils/common_widgets/view_attachment/viewers/audio_video_attachment_viewer_interactor.dart' + as _i130; +import 'package:flutter_parent/utils/common_widgets/web_view/web_content_interactor.dart' + as _i93; +import 'package:flutter_parent/utils/db/calendar_filter_db.dart' as _i48; +import 'package:flutter_parent/utils/db/reminder_db.dart' as _i85; +import 'package:flutter_parent/utils/db/user_colors_db.dart' as _i89; +import 'package:flutter_parent/utils/notification_util.dart' as _i75; +import 'package:flutter_parent/utils/old_app_migration.dart' as _i137; +import 'package:flutter_parent/utils/permission_handler.dart' as _i131; +import 'package:flutter_parent/utils/qr_utils.dart' as _i13; +import 'package:flutter_parent/utils/quick_nav.dart' as _i83; +import 'package:flutter_parent/utils/remote_config_utils.dart' as _i141; +import 'package:flutter_parent/utils/url_launcher.dart' as _i88; +import 'package:flutter_parent/utils/veneers/android_intent_veneer.dart' + as _i29; +import 'package:flutter_parent/utils/veneers/barcode_scan_veneer.dart' as _i45; +import 'package:flutter_parent/utils/veneers/flutter_downloader_veneer.dart' + as _i132; +import 'package:flutter_parent/utils/veneers/flutter_snackbar_veneer.dart' + as _i87; +import 'package:flutter_parent/utils/veneers/path_provider_veneer.dart' + as _i110; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i67; +import 'package:path_provider/path_provider.dart' as _i111; +import 'package:permission_handler/permission_handler.dart' as _i119; +import 'package:sqflite/sqflite.dart' as _i5; +import 'package:timezone/timezone.dart' as _i81; +import 'package:tuple/tuple.dart' as _i115; +import 'package:video_player/video_player.dart' as _i21; +import 'package:webview_flutter/webview_flutter.dart' as _i16; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAssignmentDetails_1 extends _i1.SmartFake + implements _i3.AssignmentDetails { + _FakeAssignmentDetails_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeScanResult_2 extends _i1.SmartFake implements _i4.ScanResult { + _FakeScanResult_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDatabase_3 extends _i1.SmartFake implements _i5.Database { + _FakeDatabase_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeGradeDetails_4 extends _i1.SmartFake implements _i6.GradeDetails { + _FakeGradeDetails_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCreateConversationData_5 extends _i1.SmartFake + implements _i7.CreateConversationData { + _FakeCreateConversationData_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFuture_6 extends _i1.SmartFake implements _i8.Future { + _FakeFuture_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeQueryCursor_7 extends _i1.SmartFake implements _i5.QueryCursor { + _FakeQueryCursor_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBatch_8 extends _i1.SmartFake implements _i5.Batch { + _FakeBatch_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBaseOptions_9 extends _i1.SmartFake implements _i2.BaseOptions { + _FakeBaseOptions_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpClientAdapter_10 extends _i1.SmartFake + implements _i2.HttpClientAdapter { + _FakeHttpClientAdapter_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeTransformer_11 extends _i1.SmartFake implements _i2.Transformer { + _FakeTransformer_11( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInterceptors_12 extends _i1.SmartFake implements _i2.Interceptors { + _FakeInterceptors_12( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFirebaseApp_13 extends _i1.SmartFake implements _i9.FirebaseApp { + _FakeFirebaseApp_13( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDuration_14 extends _i1.SmartFake implements Duration { + _FakeDuration_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpClientRequest_15 extends _i1.SmartFake + implements _i10.HttpClientRequest { + _FakeHttpClientRequest_15( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUri_16 extends _i1.SmartFake implements Uri { + _FakeUri_16( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpHeaders_17 extends _i1.SmartFake implements _i10.HttpHeaders { + _FakeHttpHeaders_17( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpClientResponse_18 extends _i1.SmartFake + implements _i11.HttpClientResponse { + _FakeHttpClientResponse_18( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeEncoding_19 extends _i1.SmartFake implements _i12.Encoding { + _FakeEncoding_19( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSocket_20 extends _i1.SmartFake implements _i10.Socket { + _FakeSocket_20( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamSubscription_21 extends _i1.SmartFake + implements _i8.StreamSubscription { + _FakeStreamSubscription_21( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeQRPairingScanResult_22 extends _i1.SmartFake + implements _i13.QRPairingScanResult { + _FakeQRPairingScanResult_22( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDateTime_23 extends _i1.SmartFake implements DateTime { + _FakeDateTime_23( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRemoteConfigSettings_24 extends _i1.SmartFake + implements _i14.RemoteConfigSettings { + _FakeRemoteConfigSettings_24( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRemoteConfigValue_25 extends _i1.SmartFake + implements _i14.RemoteConfigValue { + _FakeRemoteConfigValue_25( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeMethodChannel_26 extends _i1.SmartFake + implements _i15.MethodChannel { + _FakeMethodChannel_26( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJavascriptChannel_27 extends _i1.SmartFake + implements _i16.JavascriptChannel { + _FakeJavascriptChannel_27( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFile_28 extends _i1.SmartFake implements _i10.File { + _FakeFile_28( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDirectory_29 extends _i1.SmartFake implements _i10.Directory { + _FakeDirectory_29( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_30 extends _i1.SmartFake implements _i17.Widget { + _FakeWidget_30( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString( + {_i11.DiagnosticLevel? minLevel = _i11.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_31 extends _i1.SmartFake + implements _i17.InheritedWidget { + _FakeInheritedWidget_31( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString( + {_i11.DiagnosticLevel? minLevel = _i11.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_32 extends _i1.SmartFake + implements _i11.DiagnosticsNode { + _FakeDiagnosticsNode_32( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i11.TextTreeConfiguration? parentConfiguration, + _i11.DiagnosticLevel? minLevel = _i11.DiagnosticLevel.info, + }) => + super.toString(); +} + +class _FakeInboxCountNotifier_33 extends _i1.SmartFake + implements _i18.InboxCountNotifier { + _FakeInboxCountNotifier_33( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAlertCountNotifier_34 extends _i1.SmartFake + implements _i19.AlertCountNotifier { + _FakeAlertCountNotifier_34( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBarcodeScanResult_35 extends _i1.SmartFake + implements _i20.BarcodeScanResult { + _FakeBarcodeScanResult_35( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCancelToken_36 extends _i1.SmartFake implements _i2.CancelToken { + _FakeCancelToken_36( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDioError_37 extends _i1.SmartFake implements _i2.DioError { + _FakeDioError_37( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeVideoPlayerValue_38 extends _i1.SmartFake + implements _i21.VideoPlayerValue { + _FakeVideoPlayerValue_38( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInterceptorState_39 extends _i1.SmartFake + implements _i22.InterceptorState { + _FakeInterceptorState_39( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [AccountsApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAccountsApi extends _i1.Mock implements _i23.AccountsApi { + @override + _i8.Future?> searchDomains(String? query) => + (super.noSuchMethod( + Invocation.method( + #searchDomains, + [query], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i25.TermsOfService?> getTermsOfService() => (super.noSuchMethod( + Invocation.method( + #getTermsOfService, + [], + ), + returnValue: _i8.Future<_i25.TermsOfService?>.value(), + returnValueForMissingStub: _i8.Future<_i25.TermsOfService?>.value(), + ) as _i8.Future<_i25.TermsOfService?>); + @override + _i8.Future<_i25.TermsOfService?> getTermsOfServiceForAccount( + String? accountId, + String? domain, + ) => + (super.noSuchMethod( + Invocation.method( + #getTermsOfServiceForAccount, + [ + accountId, + domain, + ], + ), + returnValue: _i8.Future<_i25.TermsOfService?>.value(), + returnValueForMissingStub: _i8.Future<_i25.TermsOfService?>.value(), + ) as _i8.Future<_i25.TermsOfService?>); + @override + _i8.Future<_i26.AccountPermissions?> getAccountPermissions() => + (super.noSuchMethod( + Invocation.method( + #getAccountPermissions, + [], + ), + returnValue: _i8.Future<_i26.AccountPermissions?>.value(), + returnValueForMissingStub: _i8.Future<_i26.AccountPermissions?>.value(), + ) as _i8.Future<_i26.AccountPermissions?>); + @override + _i8.Future getPairingAllowed() => (super.noSuchMethod( + Invocation.method( + #getPairingAllowed, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future<_i2.Response> createNewAccount( + String? accountId, + String? pairingCode, + String? fullname, + String? email, + String? password, + String? domain, + ) => + (super.noSuchMethod( + Invocation.method( + #createNewAccount, + [ + accountId, + pairingCode, + fullname, + email, + password, + domain, + ], + ), + returnValue: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #createNewAccount, + [ + accountId, + pairingCode, + fullname, + email, + password, + domain, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #createNewAccount, + [ + accountId, + pairingCode, + fullname, + email, + password, + domain, + ], + ), + )), + ) as _i8.Future<_i2.Response>); +} + +/// A class which mocks [AccountCreationInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAccountCreationInteractor extends _i1.Mock + implements _i27.AccountCreationInteractor { + @override + _i8.Future<_i25.TermsOfService?> getToSForAccount( + String? accountId, + String? domain, + ) => + (super.noSuchMethod( + Invocation.method( + #getToSForAccount, + [ + accountId, + domain, + ], + ), + returnValue: _i8.Future<_i25.TermsOfService?>.value(), + returnValueForMissingStub: _i8.Future<_i25.TermsOfService?>.value(), + ) as _i8.Future<_i25.TermsOfService?>); + @override + _i8.Future<_i2.Response> createNewAccount( + String? accountId, + String? pairingCode, + String? fullname, + String? email, + String? password, + String? domain, + ) => + (super.noSuchMethod( + Invocation.method( + #createNewAccount, + [ + accountId, + pairingCode, + fullname, + email, + password, + domain, + ], + ), + returnValue: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #createNewAccount, + [ + accountId, + pairingCode, + fullname, + email, + password, + domain, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #createNewAccount, + [ + accountId, + pairingCode, + fullname, + email, + password, + domain, + ], + ), + )), + ) as _i8.Future<_i2.Response>); +} + +/// A class which mocks [Analytics]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAnalytics extends _i1.Mock implements _i28.Analytics { + @override + void setCurrentScreen(String? screenName) => super.noSuchMethod( + Invocation.method( + #setCurrentScreen, + [screenName], + ), + returnValueForMissingStub: null, + ); + @override + void logEvent( + String? event, { + Map? extras = const {}, + }) => + super.noSuchMethod( + Invocation.method( + #logEvent, + [event], + {#extras: extras}, + ), + returnValueForMissingStub: null, + ); + @override + void logMessage(String? message) => super.noSuchMethod( + Invocation.method( + #logMessage, + [message], + ), + returnValueForMissingStub: null, + ); + @override + void setEnvironmentProperties() => super.noSuchMethod( + Invocation.method( + #setEnvironmentProperties, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AndroidIntentVeneer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAndroidIntentVeneer extends _i1.Mock + implements _i29.AndroidIntentVeneer { + @override + dynamic launch(_i30.AndroidIntent? intent) => super.noSuchMethod( + Invocation.method( + #launch, + [intent], + ), + returnValueForMissingStub: null, + ); + @override + dynamic launchPhone(String? phoneNumber) => super.noSuchMethod( + Invocation.method( + #launchPhone, + [phoneNumber], + ), + returnValueForMissingStub: null, + ); + @override + dynamic launchEmail(String? url) => super.noSuchMethod( + Invocation.method( + #launchEmail, + [url], + ), + returnValueForMissingStub: null, + ); + @override + dynamic launchEmailWithBody( + String? subject, + String? emailBody, { + String? recipientEmail = r'mobilesupport@instructure.com', + }) => + super.noSuchMethod( + Invocation.method( + #launchEmailWithBody, + [ + subject, + emailBody, + ], + {#recipientEmail: recipientEmail}, + ), + returnValueForMissingStub: null, + ); + @override + String encodeQueryParameters(Map? params) => + (super.noSuchMethod( + Invocation.method( + #encodeQueryParameters, + [params], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); +} + +/// A class which mocks [AlertsApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAlertsApi extends _i1.Mock implements _i31.AlertsApi { + @override + _i8.Future?> getAlertsDepaginated( + String? studentId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getAlertsDepaginated, + [ + studentId, + forceRefresh, + ], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i32.Alert?> updateAlertWorkflow( + String? studentId, + String? alertId, + String? workflowState, + ) => + (super.noSuchMethod( + Invocation.method( + #updateAlertWorkflow, + [ + studentId, + alertId, + workflowState, + ], + ), + returnValue: _i8.Future<_i32.Alert?>.value(), + returnValueForMissingStub: _i8.Future<_i32.Alert?>.value(), + ) as _i8.Future<_i32.Alert?>); + @override + _i8.Future<_i33.UnreadCount?> getUnreadCount(String? studentId) => + (super.noSuchMethod( + Invocation.method( + #getUnreadCount, + [studentId], + ), + returnValue: _i8.Future<_i33.UnreadCount?>.value(), + returnValueForMissingStub: _i8.Future<_i33.UnreadCount?>.value(), + ) as _i8.Future<_i33.UnreadCount?>); + @override + _i8.Future?> getAlertThresholds( + String? studentId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getAlertThresholds, + [ + studentId, + forceRefresh, + ], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: + _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i34.AlertThreshold?> deleteAlert( + _i34.AlertThreshold? threshold) => + (super.noSuchMethod( + Invocation.method( + #deleteAlert, + [threshold], + ), + returnValue: _i8.Future<_i34.AlertThreshold?>.value(), + returnValueForMissingStub: _i8.Future<_i34.AlertThreshold?>.value(), + ) as _i8.Future<_i34.AlertThreshold?>); + @override + _i8.Future<_i34.AlertThreshold?> createThreshold( + _i32.AlertType? type, + String? studentId, { + String? value, + }) => + (super.noSuchMethod( + Invocation.method( + #createThreshold, + [ + type, + studentId, + ], + {#value: value}, + ), + returnValue: _i8.Future<_i34.AlertThreshold?>.value(), + returnValueForMissingStub: _i8.Future<_i34.AlertThreshold?>.value(), + ) as _i8.Future<_i34.AlertThreshold?>); +} + +/// A class which mocks [AlertCountNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAlertCountNotifier extends _i1.Mock + implements _i19.AlertCountNotifier { + @override + int get value => (super.noSuchMethod( + Invocation.getter(#value), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set value(int? newValue) => super.noSuchMethod( + Invocation.setter( + #value, + newValue, + ), + returnValueForMissingStub: null, + ); + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + dynamic update(String? studentId) => super.noSuchMethod( + Invocation.method( + #update, + [studentId], + ), + returnValueForMissingStub: null, + ); + @override + void addListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AssignmentApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAssignmentApi extends _i1.Mock implements _i36.AssignmentApi { + @override + _i8.Future?> getAssignmentsWithSubmissionsDepaginated( + int? courseId, + int? studentId, + ) => + (super.noSuchMethod( + Invocation.method( + #getAssignmentsWithSubmissionsDepaginated, + [ + courseId, + studentId, + ], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future?> + getAssignmentGroupsWithSubmissionsDepaginated( + String? courseId, + String? studentId, + String? gradingPeriodId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getAssignmentGroupsWithSubmissionsDepaginated, + [ + courseId, + studentId, + gradingPeriodId, + ], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: + _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i39.PagedList<_i37.Assignment>?> + getAssignmentsWithSubmissionsPaged( + String? courseId, + String? studentId, + ) => + (super.noSuchMethod( + Invocation.method( + #getAssignmentsWithSubmissionsPaged, + [ + courseId, + studentId, + ], + ), + returnValue: _i8.Future<_i39.PagedList<_i37.Assignment>?>.value(), + returnValueForMissingStub: + _i8.Future<_i39.PagedList<_i37.Assignment>?>.value(), + ) as _i8.Future<_i39.PagedList<_i37.Assignment>?>); + @override + _i8.Future<_i37.Assignment?> getAssignment( + String? courseId, + String? assignmentId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getAssignment, + [ + courseId, + assignmentId, + ], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i37.Assignment?>.value(), + returnValueForMissingStub: _i8.Future<_i37.Assignment?>.value(), + ) as _i8.Future<_i37.Assignment?>); +} + +/// A class which mocks [AssignmentDetailsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAssignmentDetailsInteractor extends _i1.Mock + implements _i3.AssignmentDetailsInteractor { + @override + _i8.Future<_i3.AssignmentDetails?> loadAssignmentDetails( + bool? forceRefresh, + String? courseId, + String? assignmentId, + String? studentId, + ) => + (super.noSuchMethod( + Invocation.method( + #loadAssignmentDetails, + [ + forceRefresh, + courseId, + assignmentId, + studentId, + ], + ), + returnValue: _i8.Future<_i3.AssignmentDetails?>.value(), + returnValueForMissingStub: _i8.Future<_i3.AssignmentDetails?>.value(), + ) as _i8.Future<_i3.AssignmentDetails?>); + @override + _i8.Future<_i3.AssignmentDetails> loadQuizDetails( + bool? forceRefresh, + String? courseId, + String? assignmentId, + String? studentId, + ) => + (super.noSuchMethod( + Invocation.method( + #loadQuizDetails, + [ + forceRefresh, + courseId, + assignmentId, + studentId, + ], + ), + returnValue: + _i8.Future<_i3.AssignmentDetails>.value(_FakeAssignmentDetails_1( + this, + Invocation.method( + #loadQuizDetails, + [ + forceRefresh, + courseId, + assignmentId, + studentId, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i3.AssignmentDetails>.value(_FakeAssignmentDetails_1( + this, + Invocation.method( + #loadQuizDetails, + [ + forceRefresh, + courseId, + assignmentId, + studentId, + ], + ), + )), + ) as _i8.Future<_i3.AssignmentDetails>); + @override + _i8.Future<_i40.Reminder?> loadReminder(String? assignmentId) => + (super.noSuchMethod( + Invocation.method( + #loadReminder, + [assignmentId], + ), + returnValue: _i8.Future<_i40.Reminder?>.value(), + returnValueForMissingStub: _i8.Future<_i40.Reminder?>.value(), + ) as _i8.Future<_i40.Reminder?>); + @override + _i8.Future createReminder( + _i41.AppLocalizations? l10n, + DateTime? date, + String? assignmentId, + String? courseId, + String? title, + String? body, + ) => + (super.noSuchMethod( + Invocation.method( + #createReminder, + [ + l10n, + date, + assignmentId, + courseId, + title, + body, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteReminder(_i40.Reminder? reminder) => + (super.noSuchMethod( + Invocation.method( + #deleteReminder, + [reminder], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [AuthApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthApi extends _i1.Mock implements _i42.AuthApi { + @override + _i8.Future<_i43.CanvasToken?> refreshToken() => (super.noSuchMethod( + Invocation.method( + #refreshToken, + [], + ), + returnValue: _i8.Future<_i43.CanvasToken?>.value(), + returnValueForMissingStub: _i8.Future<_i43.CanvasToken?>.value(), + ) as _i8.Future<_i43.CanvasToken?>); + @override + _i8.Future<_i43.CanvasToken?> getTokens( + _i44.MobileVerifyResult? verifyResult, + String? requestCode, + ) => + (super.noSuchMethod( + Invocation.method( + #getTokens, + [ + verifyResult, + requestCode, + ], + ), + returnValue: _i8.Future<_i43.CanvasToken?>.value(), + returnValueForMissingStub: _i8.Future<_i43.CanvasToken?>.value(), + ) as _i8.Future<_i43.CanvasToken?>); + @override + _i8.Future<_i44.MobileVerifyResult?> mobileVerify( + String? domain, { + bool? forceBetaDomain = false, + }) => + (super.noSuchMethod( + Invocation.method( + #mobileVerify, + [domain], + {#forceBetaDomain: forceBetaDomain}, + ), + returnValue: _i8.Future<_i44.MobileVerifyResult?>.value(), + returnValueForMissingStub: _i8.Future<_i44.MobileVerifyResult?>.value(), + ) as _i8.Future<_i44.MobileVerifyResult?>); + @override + _i8.Future deleteToken( + String? domain, + String? token, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteToken, + [ + domain, + token, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [BarcodeScanVeneer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBarcodeScanVeneer extends _i1.Mock implements _i45.BarcodeScanVeneer { + @override + _i8.Future<_i4.ScanResult> scanBarcode() => (super.noSuchMethod( + Invocation.method( + #scanBarcode, + [], + ), + returnValue: _i8.Future<_i4.ScanResult>.value(_FakeScanResult_2( + this, + Invocation.method( + #scanBarcode, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i4.ScanResult>.value(_FakeScanResult_2( + this, + Invocation.method( + #scanBarcode, + [], + ), + )), + ) as _i8.Future<_i4.ScanResult>); + @override + _i8.Future getNumberOfCameras() => (super.noSuchMethod( + Invocation.method( + #getNumberOfCameras, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [CalendarEventsApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCalendarEventsApi extends _i1.Mock implements _i46.CalendarEventsApi { + @override + _i8.Future?> getAllCalendarEvents({ + bool? allEvents = false, + String? type = r'event', + String? startDate = null, + String? endDate = null, + List? contexts = const [], + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getAllCalendarEvents, + [], + { + #allEvents: allEvents, + #type: type, + #startDate: startDate, + #endDate: endDate, + #contexts: contexts, + #forceRefresh: forceRefresh, + }, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i47.ScheduleItem?> getEvent( + String? eventId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getEvent, + [ + eventId, + forceRefresh, + ], + ), + returnValue: _i8.Future<_i47.ScheduleItem?>.value(), + returnValueForMissingStub: _i8.Future<_i47.ScheduleItem?>.value(), + ) as _i8.Future<_i47.ScheduleItem?>); + @override + _i8.Future?> getUserCalendarItems( + String? userId, + DateTime? startDay, + DateTime? endDay, + String? type, { + Set? contexts = const {}, + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getUserCalendarItems, + [ + userId, + startDay, + endDay, + type, + ], + { + #contexts: contexts, + #forceRefresh: forceRefresh, + }, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + Map getQueryParams( + DateTime? startDay, + DateTime? endDay, + String? type, { + Set? contexts = const {}, + bool? includeSubmissions = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getQueryParams, + [ + startDay, + endDay, + type, + ], + { + #contexts: contexts, + #includeSubmissions: includeSubmissions, + }, + ), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); +} + +/// A class which mocks [CalendarFilterDb]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCalendarFilterDb extends _i1.Mock implements _i48.CalendarFilterDb { + @override + _i5.Database get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeDatabase_3( + this, + Invocation.getter(#db), + ), + returnValueForMissingStub: _FakeDatabase_3( + this, + Invocation.getter(#db), + ), + ) as _i5.Database); + @override + set db(_i5.Database? _db) => super.noSuchMethod( + Invocation.setter( + #db, + _db, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i49.CalendarFilter?> insertOrUpdate(_i49.CalendarFilter? data) => + (super.noSuchMethod( + Invocation.method( + #insertOrUpdate, + [data], + ), + returnValue: _i8.Future<_i49.CalendarFilter?>.value(), + returnValueForMissingStub: _i8.Future<_i49.CalendarFilter?>.value(), + ) as _i8.Future<_i49.CalendarFilter?>); + @override + _i8.Future<_i49.CalendarFilter?> getById(int? id) => (super.noSuchMethod( + Invocation.method( + #getById, + [id], + ), + returnValue: _i8.Future<_i49.CalendarFilter?>.value(), + returnValueForMissingStub: _i8.Future<_i49.CalendarFilter?>.value(), + ) as _i8.Future<_i49.CalendarFilter?>); + @override + _i8.Future<_i49.CalendarFilter?> getByObserveeId( + String? userDomain, + String? userId, + String? observeeId, + ) => + (super.noSuchMethod( + Invocation.method( + #getByObserveeId, + [ + userDomain, + userId, + observeeId, + ], + ), + returnValue: _i8.Future<_i49.CalendarFilter?>.value(), + returnValueForMissingStub: _i8.Future<_i49.CalendarFilter?>.value(), + ) as _i8.Future<_i49.CalendarFilter?>); + @override + _i8.Future deleteById(int? id) => (super.noSuchMethod( + Invocation.method( + #deleteById, + [id], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future deleteAllForUser( + String? userDomain, + String? userId, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteAllForUser, + [ + userDomain, + userId, + ], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [CalendarFilterListInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCalendarFilterListInteractor extends _i1.Mock + implements _i50.CalendarFilterListInteractor { + @override + _i8.Future?> getCoursesForSelectedStudent( + {bool? isRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getCoursesForSelectedStudent, + [], + {#isRefresh: isRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); +} + +/// A class which mocks [CourseApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCourseApi extends _i1.Mock implements _i52.CourseApi { + @override + _i8.Future?> getObserveeCourses( + {bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getObserveeCourses, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i51.Course?> getCourse( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getCourse, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i51.Course?>.value(), + returnValueForMissingStub: _i8.Future<_i51.Course?>.value(), + ) as _i8.Future<_i51.Course?>); + @override + _i8.Future<_i53.GradingPeriodResponse?> getGradingPeriods( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getGradingPeriods, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i53.GradingPeriodResponse?>.value(), + returnValueForMissingStub: + _i8.Future<_i53.GradingPeriodResponse?>.value(), + ) as _i8.Future<_i53.GradingPeriodResponse?>); + @override + _i8.Future?> getCourseTabs( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getCourseTabs, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i55.CourseSettings?> getCourseSettings( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getCourseSettings, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i55.CourseSettings?>.value(), + returnValueForMissingStub: _i8.Future<_i55.CourseSettings?>.value(), + ) as _i8.Future<_i55.CourseSettings?>); + @override + _i8.Future<_i56.CoursePermissions?> getCoursePermissions( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getCoursePermissions, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i56.CoursePermissions?>.value(), + returnValueForMissingStub: _i8.Future<_i56.CoursePermissions?>.value(), + ) as _i8.Future<_i56.CoursePermissions?>); +} + +/// A class which mocks [CourseDetailsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCourseDetailsInteractor extends _i1.Mock + implements _i57.CourseDetailsInteractor { + @override + _i8.Future<_i51.Course?> loadCourse( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadCourse, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i51.Course?>.value(), + returnValueForMissingStub: _i8.Future<_i51.Course?>.value(), + ) as _i8.Future<_i51.Course?>); + @override + _i8.Future?> loadCourseTabs( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadCourseTabs, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i55.CourseSettings?> loadCourseSettings( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadCourseSettings, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i55.CourseSettings?>.value(), + returnValueForMissingStub: _i8.Future<_i55.CourseSettings?>.value(), + ) as _i8.Future<_i55.CourseSettings?>); + @override + _i8.Future?> loadAssignmentGroups( + String? courseId, + String? studentId, + String? gradingPeriodId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadAssignmentGroups, + [ + courseId, + studentId, + gradingPeriodId, + ], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: + _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i53.GradingPeriodResponse?> loadGradingPeriods( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadGradingPeriods, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i53.GradingPeriodResponse?>.value(), + returnValueForMissingStub: + _i8.Future<_i53.GradingPeriodResponse?>.value(), + ) as _i8.Future<_i53.GradingPeriodResponse?>); + @override + _i8.Future?> loadEnrollmentsForGradingPeriod( + String? courseId, + String? studentId, + String? gradingPeriodId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadEnrollmentsForGradingPeriod, + [ + courseId, + studentId, + gradingPeriodId, + ], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future?> loadScheduleItems( + String? courseId, + String? type, + bool? refresh, + ) => + (super.noSuchMethod( + Invocation.method( + #loadScheduleItems, + [ + courseId, + type, + refresh, + ], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i59.CanvasPage?> loadFrontPage( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadFrontPage, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i59.CanvasPage?>.value(), + returnValueForMissingStub: _i8.Future<_i59.CanvasPage?>.value(), + ) as _i8.Future<_i59.CanvasPage?>); +} + +/// A class which mocks [CoursesInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCoursesInteractor extends _i1.Mock implements _i60.CoursesInteractor { + @override + _i8.Future?> getCourses({ + bool? isRefresh = false, + String? studentId = null, + }) => + (super.noSuchMethod( + Invocation.method( + #getCourses, + [], + { + #isRefresh: isRefresh, + #studentId: studentId, + }, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); +} + +/// A class which mocks [CourseDetailsModel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCourseDetailsModel extends _i1.Mock + implements _i6.CourseDetailsModel { + @override + set student(_i61.User? _student) => super.noSuchMethod( + Invocation.setter( + #student, + _student, + ), + returnValueForMissingStub: null, + ); + @override + String get courseId => (super.noSuchMethod( + Invocation.getter(#courseId), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + set courseId(String? _courseId) => super.noSuchMethod( + Invocation.setter( + #courseId, + _courseId, + ), + returnValueForMissingStub: null, + ); + @override + set course(_i51.Course? _course) => super.noSuchMethod( + Invocation.setter( + #course, + _course, + ), + returnValueForMissingStub: null, + ); + @override + set courseSettings(_i55.CourseSettings? _courseSettings) => + super.noSuchMethod( + Invocation.setter( + #courseSettings, + _courseSettings, + ), + returnValueForMissingStub: null, + ); + @override + set tabs(List<_i54.CourseTab>? _tabs) => super.noSuchMethod( + Invocation.setter( + #tabs, + _tabs, + ), + returnValueForMissingStub: null, + ); + @override + bool get forceRefresh => (super.noSuchMethod( + Invocation.getter(#forceRefresh), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set forceRefresh(bool? _forceRefresh) => super.noSuchMethod( + Invocation.setter( + #forceRefresh, + _forceRefresh, + ), + returnValueForMissingStub: null, + ); + @override + bool get hasHomePageAsFrontPage => (super.noSuchMethod( + Invocation.getter(#hasHomePageAsFrontPage), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + bool get hasHomePageAsSyllabus => (super.noSuchMethod( + Invocation.getter(#hasHomePageAsSyllabus), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + bool get showSummary => (super.noSuchMethod( + Invocation.getter(#showSummary), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + bool get restrictQuantitativeData => (super.noSuchMethod( + Invocation.getter(#restrictQuantitativeData), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i62.ViewState get state => (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i62.ViewState.Idle, + returnValueForMissingStub: _i62.ViewState.Idle, + ) as _i62.ViewState); + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i8.Future loadData({bool? refreshCourse = false}) => + (super.noSuchMethod( + Invocation.method( + #loadData, + [], + {#refreshCourse: refreshCourse}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future<_i6.GradeDetails> loadAssignments({bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #loadAssignments, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i6.GradeDetails>.value(_FakeGradeDetails_4( + this, + Invocation.method( + #loadAssignments, + [], + {#forceRefresh: forceRefresh}, + ), + )), + returnValueForMissingStub: + _i8.Future<_i6.GradeDetails>.value(_FakeGradeDetails_4( + this, + Invocation.method( + #loadAssignments, + [], + {#forceRefresh: forceRefresh}, + ), + )), + ) as _i8.Future<_i6.GradeDetails>); + @override + _i8.Future?> loadSummary({bool? refresh = false}) => + (super.noSuchMethod( + Invocation.method( + #loadSummary, + [], + {#refresh: refresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + int tabCount() => (super.noSuchMethod( + Invocation.method( + #tabCount, + [], + ), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + void setState({required _i62.ViewState? viewState}) => super.noSuchMethod( + Invocation.method( + #setState, + [], + {#viewState: viewState}, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future work(_i8.Future Function()? loadBlock) => + (super.noSuchMethod( + Invocation.method( + #work, + [loadBlock], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + void addListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CourseRoutingShellInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCourseRoutingShellInteractor extends _i1.Mock + implements _i63.CourseRoutingShellInteractor { + @override + _i8.Future<_i63.CourseShellData?> loadCourseShell( + _i64.CourseShellType? type, + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #loadCourseShell, + [ + type, + courseId, + ], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i63.CourseShellData?>.value(), + returnValueForMissingStub: _i8.Future<_i63.CourseShellData?>.value(), + ) as _i8.Future<_i63.CourseShellData?>); +} + +/// A class which mocks [CreateConversationInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCreateConversationInteractor extends _i1.Mock + implements _i7.CreateConversationInteractor { + @override + _i8.Future<_i7.CreateConversationData> loadData( + String? courseId, + String? studentId, + ) => + (super.noSuchMethod( + Invocation.method( + #loadData, + [ + courseId, + studentId, + ], + ), + returnValue: _i8.Future<_i7.CreateConversationData>.value( + _FakeCreateConversationData_5( + this, + Invocation.method( + #loadData, + [ + courseId, + studentId, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i7.CreateConversationData>.value( + _FakeCreateConversationData_5( + this, + Invocation.method( + #loadData, + [ + courseId, + studentId, + ], + ), + )), + ) as _i8.Future<_i7.CreateConversationData>); + @override + _i8.Future<_i65.Conversation?> createConversation( + String? courseId, + List? recipientIds, + String? subject, + String? body, + List? attachmentIds, + ) => + (super.noSuchMethod( + Invocation.method( + #createConversation, + [ + courseId, + recipientIds, + subject, + body, + attachmentIds, + ], + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); + @override + _i8.Future<_i66.AttachmentHandler?> addAttachment( + _i17.BuildContext? context) => + (super.noSuchMethod( + Invocation.method( + #addAttachment, + [context], + ), + returnValue: _i8.Future<_i66.AttachmentHandler?>.value(), + returnValueForMissingStub: _i8.Future<_i66.AttachmentHandler?>.value(), + ) as _i8.Future<_i66.AttachmentHandler?>); +} + +/// A class which mocks [Database]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDatabase extends _i1.Mock implements _i5.Database { + @override + String get path => (super.noSuchMethod( + Invocation.getter(#path), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + bool get isOpen => (super.noSuchMethod( + Invocation.getter(#isOpen), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i5.Database get database => (super.noSuchMethod( + Invocation.getter(#database), + returnValue: _FakeDatabase_3( + this, + Invocation.getter(#database), + ), + returnValueForMissingStub: _FakeDatabase_3( + this, + Invocation.getter(#database), + ), + ) as _i5.Database); + @override + _i8.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future transaction( + _i8.Future Function(_i5.Transaction)? action, { + bool? exclusive, + }) => + (super.noSuchMethod( + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + returnValue: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + ), + (T v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + ), + returnValueForMissingStub: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + ), + (T v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + ), + ) as _i8.Future); + @override + _i8.Future devInvokeMethod( + String? method, [ + Object? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + returnValue: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + ), + (T v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + ), + returnValueForMissingStub: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + ), + (T v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + ), + ) as _i8.Future); + @override + _i8.Future devInvokeSqlMethod( + String? method, + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + returnValue: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + ), + (T v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + ), + returnValueForMissingStub: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + ), + (T v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + ), + ) as _i8.Future); + @override + _i8.Future execute( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #execute, + [ + sql, + arguments, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future rawInsert( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawInsert, + [ + sql, + arguments, + ], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future insert( + String? table, + Map? values, { + String? nullColumnHack, + _i5.ConflictAlgorithm? conflictAlgorithm, + }) => + (super.noSuchMethod( + Invocation.method( + #insert, + [ + table, + values, + ], + { + #nullColumnHack: nullColumnHack, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future>> query( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) => + (super.noSuchMethod( + Invocation.method( + #query, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + }, + ), + returnValue: _i8.Future>>.value( + >[]), + returnValueForMissingStub: _i8.Future>>.value( + >[]), + ) as _i8.Future>>); + @override + _i8.Future>> rawQuery( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawQuery, + [ + sql, + arguments, + ], + ), + returnValue: _i8.Future>>.value( + >[]), + returnValueForMissingStub: _i8.Future>>.value( + >[]), + ) as _i8.Future>>); + @override + _i8.Future<_i5.QueryCursor> rawQueryCursor( + String? sql, + List? arguments, { + int? bufferSize, + }) => + (super.noSuchMethod( + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + returnValue: _i8.Future<_i5.QueryCursor>.value(_FakeQueryCursor_7( + this, + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + )), + returnValueForMissingStub: + _i8.Future<_i5.QueryCursor>.value(_FakeQueryCursor_7( + this, + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + )), + ) as _i8.Future<_i5.QueryCursor>); + @override + _i8.Future<_i5.QueryCursor> queryCursor( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + int? bufferSize, + }) => + (super.noSuchMethod( + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + returnValue: _i8.Future<_i5.QueryCursor>.value(_FakeQueryCursor_7( + this, + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i5.QueryCursor>.value(_FakeQueryCursor_7( + this, + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + )), + ) as _i8.Future<_i5.QueryCursor>); + @override + _i8.Future rawUpdate( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawUpdate, + [ + sql, + arguments, + ], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future update( + String? table, + Map? values, { + String? where, + List? whereArgs, + _i5.ConflictAlgorithm? conflictAlgorithm, + }) => + (super.noSuchMethod( + Invocation.method( + #update, + [ + table, + values, + ], + { + #where: where, + #whereArgs: whereArgs, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future rawDelete( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawDelete, + [ + sql, + arguments, + ], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future delete( + String? table, { + String? where, + List? whereArgs, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [table], + { + #where: where, + #whereArgs: whereArgs, + }, + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i5.Batch batch() => (super.noSuchMethod( + Invocation.method( + #batch, + [], + ), + returnValue: _FakeBatch_8( + this, + Invocation.method( + #batch, + [], + ), + ), + returnValueForMissingStub: _FakeBatch_8( + this, + Invocation.method( + #batch, + [], + ), + ), + ) as _i5.Batch); +} + +/// A class which mocks [Dio]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDio extends _i1.Mock implements _i2.Dio { + @override + _i2.BaseOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeBaseOptions_9( + this, + Invocation.getter(#options), + ), + returnValueForMissingStub: _FakeBaseOptions_9( + this, + Invocation.getter(#options), + ), + ) as _i2.BaseOptions); + @override + set options(_i2.BaseOptions? _options) => super.noSuchMethod( + Invocation.setter( + #options, + _options, + ), + returnValueForMissingStub: null, + ); + @override + _i2.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( + Invocation.getter(#httpClientAdapter), + returnValue: _FakeHttpClientAdapter_10( + this, + Invocation.getter(#httpClientAdapter), + ), + returnValueForMissingStub: _FakeHttpClientAdapter_10( + this, + Invocation.getter(#httpClientAdapter), + ), + ) as _i2.HttpClientAdapter); + @override + set httpClientAdapter(_i2.HttpClientAdapter? _httpClientAdapter) => + super.noSuchMethod( + Invocation.setter( + #httpClientAdapter, + _httpClientAdapter, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Transformer get transformer => (super.noSuchMethod( + Invocation.getter(#transformer), + returnValue: _FakeTransformer_11( + this, + Invocation.getter(#transformer), + ), + returnValueForMissingStub: _FakeTransformer_11( + this, + Invocation.getter(#transformer), + ), + ) as _i2.Transformer); + @override + set transformer(_i2.Transformer? _transformer) => super.noSuchMethod( + Invocation.setter( + #transformer, + _transformer, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Interceptors get interceptors => (super.noSuchMethod( + Invocation.getter(#interceptors), + returnValue: _FakeInterceptors_12( + this, + Invocation.getter(#interceptors), + ), + returnValueForMissingStub: _FakeInterceptors_12( + this, + Invocation.getter(#interceptors), + ), + ) as _i2.Interceptors); + @override + void close({bool? force = false}) => super.noSuchMethod( + Invocation.method( + #close, + [], + {#force: force}, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i2.Response> get( + String? path, { + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [path], + { + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [path], + { + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [path], + { + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> getUri( + Uri? uri, { + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #getUri, + [uri], + { + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #getUri, + [uri], + { + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #getUri, + [uri], + { + #options: options, + #cancelToken: cancelToken, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> post( + String? path, { + dynamic data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> postUri( + Uri? uri, { + dynamic data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #postUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> put( + String? path, { + dynamic data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> putUri( + Uri? uri, { + dynamic data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #putUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> head( + String? path, { + dynamic data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> headUri( + Uri? uri, { + dynamic data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #headUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> delete( + String? path, { + dynamic data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> deleteUri( + Uri? uri, { + dynamic data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + }) => + (super.noSuchMethod( + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #deleteUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> patch( + String? path, { + dynamic data, + Map? queryParameters, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [path], + { + #data: data, + #queryParameters: queryParameters, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> patchUri( + Uri? uri, { + dynamic data, + _i2.Options? options, + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patchUri, + [uri], + { + #data: data, + #options: options, + #cancelToken: cancelToken, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + void lock() => super.noSuchMethod( + Invocation.method( + #lock, + [], + ), + returnValueForMissingStub: null, + ); + @override + void unlock() => super.noSuchMethod( + Invocation.method( + #unlock, + [], + ), + returnValueForMissingStub: null, + ); + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i2.Response> download( + String? urlPath, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + Map? queryParameters, + _i2.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + dynamic data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #download, + [ + urlPath, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> downloadUri( + Uri? uri, + dynamic savePath, { + _i2.ProgressCallback? onReceiveProgress, + _i2.CancelToken? cancelToken, + bool? deleteOnError = true, + String? lengthHeader = r'content-length', + dynamic data, + _i2.Options? options, + }) => + (super.noSuchMethod( + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + returnValue: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #downloadUri, + [ + uri, + savePath, + ], + { + #onReceiveProgress: onReceiveProgress, + #cancelToken: cancelToken, + #deleteOnError: deleteOnError, + #lengthHeader: lengthHeader, + #data: data, + #options: options, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> request( + String? path, { + dynamic data, + Map? queryParameters, + _i2.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #request, + [path], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #request, + [path], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #request, + [path], + { + #data: data, + #queryParameters: queryParameters, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> requestUri( + Uri? uri, { + dynamic data, + _i2.CancelToken? cancelToken, + _i2.Options? options, + _i2.ProgressCallback? onSendProgress, + _i2.ProgressCallback? onReceiveProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #requestUri, + [uri], + { + #data: data, + #cancelToken: cancelToken, + #options: options, + #onSendProgress: onSendProgress, + #onReceiveProgress: onReceiveProgress, + }, + ), + )), + ) as _i8.Future<_i2.Response>); + @override + _i8.Future<_i2.Response> fetch(_i2.RequestOptions? requestOptions) => + (super.noSuchMethod( + Invocation.method( + #fetch, + [requestOptions], + ), + returnValue: _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #fetch, + [requestOptions], + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #fetch, + [requestOptions], + ), + )), + ) as _i8.Future<_i2.Response>); +} + +/// A class which mocks [EnrollmentsApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockEnrollmentsApi extends _i1.Mock implements _i68.EnrollmentsApi { + @override + _i8.Future?> getObserveeEnrollments( + {bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getObserveeEnrollments, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future?> getSelfEnrollments( + {bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getSelfEnrollments, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future?> getEnrollmentsByGradingPeriod( + String? courseId, + String? studentId, + String? gradingPeriodId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getEnrollmentsByGradingPeriod, + [ + courseId, + studentId, + gradingPeriodId, + ], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future pairWithStudent(String? pairingCode) => (super.noSuchMethod( + Invocation.method( + #pairWithStudent, + [pairingCode], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future unpairStudent(String? studentId) => (super.noSuchMethod( + Invocation.method( + #unpairStudent, + [studentId], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future canUnpairStudent(String? studentId) => (super.noSuchMethod( + Invocation.method( + #canUnpairStudent, + [studentId], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); +} + +/// A class which mocks [ErrorReportApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockErrorReportApi extends _i1.Mock implements _i69.ErrorReportApi { + @override + _i8.Future submitErrorReport({ + String? subject, + String? description, + String? email, + String? severity, + String? stacktrace, + String? domain, + String? name, + String? becomeUser, + String? userRoles, + }) => + (super.noSuchMethod( + Invocation.method( + #submitErrorReport, + [], + { + #subject: subject, + #description: description, + #email: email, + #severity: severity, + #stacktrace: stacktrace, + #domain: domain, + #name: name, + #becomeUser: becomeUser, + #userRoles: userRoles, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [ErrorReportInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockErrorReportInteractor extends _i1.Mock + implements _i70.ErrorReportInteractor { + @override + _i8.Future submitErrorReport( + String? subject, + String? description, + String? email, + _i70.ErrorReportSeverity? severity, + String? stacktrace, + ) => + (super.noSuchMethod( + Invocation.method( + #submitErrorReport, + [ + subject, + description, + email, + severity, + stacktrace, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [EventDetailsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockEventDetailsInteractor extends _i1.Mock + implements _i71.EventDetailsInteractor { + @override + _i8.Future<_i47.ScheduleItem?> loadEvent( + String? eventId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #loadEvent, + [ + eventId, + forceRefresh, + ], + ), + returnValue: _i8.Future<_i47.ScheduleItem?>.value(), + returnValueForMissingStub: _i8.Future<_i47.ScheduleItem?>.value(), + ) as _i8.Future<_i47.ScheduleItem?>); + @override + _i8.Future<_i40.Reminder?> loadReminder(String? eventId) => + (super.noSuchMethod( + Invocation.method( + #loadReminder, + [eventId], + ), + returnValue: _i8.Future<_i40.Reminder?>.value(), + returnValueForMissingStub: _i8.Future<_i40.Reminder?>.value(), + ) as _i8.Future<_i40.Reminder?>); + @override + _i8.Future createReminder( + _i41.AppLocalizations? l10n, + DateTime? date, + String? eventId, + String? courseId, + String? title, + String? body, + ) => + (super.noSuchMethod( + Invocation.method( + #createReminder, + [ + l10n, + date, + eventId, + courseId, + title, + body, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteReminder(_i40.Reminder? reminder) => + (super.noSuchMethod( + Invocation.method( + #deleteReminder, + [reminder], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [FirebaseCrashlytics]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseCrashlytics extends _i1.Mock + implements _i72.FirebaseCrashlytics { + @override + _i9.FirebaseApp get app => (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_13( + this, + Invocation.getter(#app), + ), + returnValueForMissingStub: _FakeFirebaseApp_13( + this, + Invocation.getter(#app), + ), + ) as _i9.FirebaseApp); + @override + set app(_i9.FirebaseApp? _app) => super.noSuchMethod( + Invocation.setter( + #app, + _app, + ), + returnValueForMissingStub: null, + ); + @override + bool get isCrashlyticsCollectionEnabled => (super.noSuchMethod( + Invocation.getter(#isCrashlyticsCollectionEnabled), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + @override + _i8.Future checkForUnsentReports() => (super.noSuchMethod( + Invocation.method( + #checkForUnsentReports, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + void crash() => super.noSuchMethod( + Invocation.method( + #crash, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future deleteUnsentReports() => (super.noSuchMethod( + Invocation.method( + #deleteUnsentReports, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future didCrashOnPreviousExecution() => (super.noSuchMethod( + Invocation.method( + #didCrashOnPreviousExecution, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future recordError( + dynamic exception, + StackTrace? stack, { + dynamic reason, + Iterable? information = const [], + bool? printDetails, + bool? fatal = false, + }) => + (super.noSuchMethod( + Invocation.method( + #recordError, + [ + exception, + stack, + ], + { + #reason: reason, + #information: information, + #printDetails: printDetails, + #fatal: fatal, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future recordFlutterError( + _i11.FlutterErrorDetails? flutterErrorDetails, { + bool? fatal = false, + }) => + (super.noSuchMethod( + Invocation.method( + #recordFlutterError, + [flutterErrorDetails], + {#fatal: fatal}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future recordFlutterFatalError( + _i11.FlutterErrorDetails? flutterErrorDetails) => + (super.noSuchMethod( + Invocation.method( + #recordFlutterFatalError, + [flutterErrorDetails], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future log(String? message) => (super.noSuchMethod( + Invocation.method( + #log, + [message], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future sendUnsentReports() => (super.noSuchMethod( + Invocation.method( + #sendUnsentReports, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setCrashlyticsCollectionEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setCrashlyticsCollectionEnabled, + [enabled], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setUserIdentifier(String? identifier) => (super.noSuchMethod( + Invocation.method( + #setUserIdentifier, + [identifier], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setCustomKey( + String? key, + Object? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setCustomKey, + [ + key, + value, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [HttpClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpClient extends _i1.Mock implements _i10.HttpClient { + @override + Duration get idleTimeout => (super.noSuchMethod( + Invocation.getter(#idleTimeout), + returnValue: _FakeDuration_14( + this, + Invocation.getter(#idleTimeout), + ), + returnValueForMissingStub: _FakeDuration_14( + this, + Invocation.getter(#idleTimeout), + ), + ) as Duration); + @override + set idleTimeout(Duration? _idleTimeout) => super.noSuchMethod( + Invocation.setter( + #idleTimeout, + _idleTimeout, + ), + returnValueForMissingStub: null, + ); + @override + set connectionTimeout(Duration? _connectionTimeout) => super.noSuchMethod( + Invocation.setter( + #connectionTimeout, + _connectionTimeout, + ), + returnValueForMissingStub: null, + ); + @override + set maxConnectionsPerHost(int? _maxConnectionsPerHost) => super.noSuchMethod( + Invocation.setter( + #maxConnectionsPerHost, + _maxConnectionsPerHost, + ), + returnValueForMissingStub: null, + ); + @override + bool get autoUncompress => (super.noSuchMethod( + Invocation.getter(#autoUncompress), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set autoUncompress(bool? _autoUncompress) => super.noSuchMethod( + Invocation.setter( + #autoUncompress, + _autoUncompress, + ), + returnValueForMissingStub: null, + ); + @override + set userAgent(String? _userAgent) => super.noSuchMethod( + Invocation.setter( + #userAgent, + _userAgent, + ), + returnValueForMissingStub: null, + ); + @override + set authenticate( + _i8.Future Function( + Uri, + String, + String?, + )? f) => + super.noSuchMethod( + Invocation.setter( + #authenticate, + f, + ), + returnValueForMissingStub: null, + ); + @override + set connectionFactory( + _i8.Future<_i10.ConnectionTask<_i10.Socket>> Function( + Uri, + String?, + int?, + )? f) => + super.noSuchMethod( + Invocation.setter( + #connectionFactory, + f, + ), + returnValueForMissingStub: null, + ); + @override + set findProxy(String Function(Uri)? f) => super.noSuchMethod( + Invocation.setter( + #findProxy, + f, + ), + returnValueForMissingStub: null, + ); + @override + set authenticateProxy( + _i8.Future Function( + String, + int, + String, + String?, + )? f) => + super.noSuchMethod( + Invocation.setter( + #authenticateProxy, + f, + ), + returnValueForMissingStub: null, + ); + @override + set badCertificateCallback( + bool Function( + _i10.X509Certificate, + String, + int, + )? callback) => + super.noSuchMethod( + Invocation.setter( + #badCertificateCallback, + callback, + ), + returnValueForMissingStub: null, + ); + @override + set keyLog(dynamic Function(String)? callback) => super.noSuchMethod( + Invocation.setter( + #keyLog, + callback, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i10.HttpClientRequest> open( + String? method, + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #open, + [ + method, + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #open, + [ + method, + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #open, + [ + method, + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> openUrl( + String? method, + Uri? url, + ) => + (super.noSuchMethod( + Invocation.method( + #openUrl, + [ + method, + url, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #openUrl, + [ + method, + url, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #openUrl, + [ + method, + url, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> get( + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #get, + [ + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #get, + [ + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #get, + [ + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> getUrl(Uri? url) => (super.noSuchMethod( + Invocation.method( + #getUrl, + [url], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #getUrl, + [url], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #getUrl, + [url], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> post( + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #post, + [ + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #post, + [ + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #post, + [ + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> postUrl(Uri? url) => (super.noSuchMethod( + Invocation.method( + #postUrl, + [url], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #postUrl, + [url], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #postUrl, + [url], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> put( + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #put, + [ + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #put, + [ + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #put, + [ + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> putUrl(Uri? url) => (super.noSuchMethod( + Invocation.method( + #putUrl, + [url], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #putUrl, + [url], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #putUrl, + [url], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> delete( + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #delete, + [ + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #delete, + [ + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #delete, + [ + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> deleteUrl(Uri? url) => (super.noSuchMethod( + Invocation.method( + #deleteUrl, + [url], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #deleteUrl, + [url], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #deleteUrl, + [url], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> patch( + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #patch, + [ + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #patch, + [ + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #patch, + [ + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> patchUrl(Uri? url) => (super.noSuchMethod( + Invocation.method( + #patchUrl, + [url], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #patchUrl, + [url], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #patchUrl, + [url], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> head( + String? host, + int? port, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #head, + [ + host, + port, + path, + ], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #head, + [ + host, + port, + path, + ], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #head, + [ + host, + port, + path, + ], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + _i8.Future<_i10.HttpClientRequest> headUrl(Uri? url) => (super.noSuchMethod( + Invocation.method( + #headUrl, + [url], + ), + returnValue: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #headUrl, + [url], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_15( + this, + Invocation.method( + #headUrl, + [url], + ), + )), + ) as _i8.Future<_i10.HttpClientRequest>); + @override + void addCredentials( + Uri? url, + String? realm, + _i10.HttpClientCredentials? credentials, + ) => + super.noSuchMethod( + Invocation.method( + #addCredentials, + [ + url, + realm, + credentials, + ], + ), + returnValueForMissingStub: null, + ); + @override + void addProxyCredentials( + String? host, + int? port, + String? realm, + _i10.HttpClientCredentials? credentials, + ) => + super.noSuchMethod( + Invocation.method( + #addProxyCredentials, + [ + host, + port, + realm, + credentials, + ], + ), + returnValueForMissingStub: null, + ); + @override + void close({bool? force = false}) => super.noSuchMethod( + Invocation.method( + #close, + [], + {#force: force}, + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [HttpClientRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpClientRequest extends _i1.Mock implements _i10.HttpClientRequest { + @override + bool get persistentConnection => (super.noSuchMethod( + Invocation.getter(#persistentConnection), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set persistentConnection(bool? _persistentConnection) => super.noSuchMethod( + Invocation.setter( + #persistentConnection, + _persistentConnection, + ), + returnValueForMissingStub: null, + ); + @override + bool get followRedirects => (super.noSuchMethod( + Invocation.getter(#followRedirects), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set followRedirects(bool? _followRedirects) => super.noSuchMethod( + Invocation.setter( + #followRedirects, + _followRedirects, + ), + returnValueForMissingStub: null, + ); + @override + int get maxRedirects => (super.noSuchMethod( + Invocation.getter(#maxRedirects), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set maxRedirects(int? _maxRedirects) => super.noSuchMethod( + Invocation.setter( + #maxRedirects, + _maxRedirects, + ), + returnValueForMissingStub: null, + ); + @override + int get contentLength => (super.noSuchMethod( + Invocation.getter(#contentLength), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set contentLength(int? _contentLength) => super.noSuchMethod( + Invocation.setter( + #contentLength, + _contentLength, + ), + returnValueForMissingStub: null, + ); + @override + bool get bufferOutput => (super.noSuchMethod( + Invocation.getter(#bufferOutput), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set bufferOutput(bool? _bufferOutput) => super.noSuchMethod( + Invocation.setter( + #bufferOutput, + _bufferOutput, + ), + returnValueForMissingStub: null, + ); + @override + String get method => (super.noSuchMethod( + Invocation.getter(#method), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + Uri get uri => (super.noSuchMethod( + Invocation.getter(#uri), + returnValue: _FakeUri_16( + this, + Invocation.getter(#uri), + ), + returnValueForMissingStub: _FakeUri_16( + this, + Invocation.getter(#uri), + ), + ) as Uri); + @override + _i10.HttpHeaders get headers => (super.noSuchMethod( + Invocation.getter(#headers), + returnValue: _FakeHttpHeaders_17( + this, + Invocation.getter(#headers), + ), + returnValueForMissingStub: _FakeHttpHeaders_17( + this, + Invocation.getter(#headers), + ), + ) as _i10.HttpHeaders); + @override + List<_i10.Cookie> get cookies => (super.noSuchMethod( + Invocation.getter(#cookies), + returnValue: <_i10.Cookie>[], + returnValueForMissingStub: <_i10.Cookie>[], + ) as List<_i10.Cookie>); + @override + _i8.Future<_i11.HttpClientResponse> get done => (super.noSuchMethod( + Invocation.getter(#done), + returnValue: _i8.Future<_i11.HttpClientResponse>.value( + _FakeHttpClientResponse_18( + this, + Invocation.getter(#done), + )), + returnValueForMissingStub: _i8.Future<_i11.HttpClientResponse>.value( + _FakeHttpClientResponse_18( + this, + Invocation.getter(#done), + )), + ) as _i8.Future<_i11.HttpClientResponse>); + @override + _i12.Encoding get encoding => (super.noSuchMethod( + Invocation.getter(#encoding), + returnValue: _FakeEncoding_19( + this, + Invocation.getter(#encoding), + ), + returnValueForMissingStub: _FakeEncoding_19( + this, + Invocation.getter(#encoding), + ), + ) as _i12.Encoding); + @override + set encoding(_i12.Encoding? _encoding) => super.noSuchMethod( + Invocation.setter( + #encoding, + _encoding, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i11.HttpClientResponse> close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i8.Future<_i11.HttpClientResponse>.value( + _FakeHttpClientResponse_18( + this, + Invocation.method( + #close, + [], + ), + )), + returnValueForMissingStub: _i8.Future<_i11.HttpClientResponse>.value( + _FakeHttpClientResponse_18( + this, + Invocation.method( + #close, + [], + ), + )), + ) as _i8.Future<_i11.HttpClientResponse>); + @override + void abort([ + Object? exception, + StackTrace? stackTrace, + ]) => + super.noSuchMethod( + Invocation.method( + #abort, + [ + exception, + stackTrace, + ], + ), + returnValueForMissingStub: null, + ); + @override + void add(List? data) => super.noSuchMethod( + Invocation.method( + #add, + [data], + ), + returnValueForMissingStub: null, + ); + @override + void write(Object? object) => super.noSuchMethod( + Invocation.method( + #write, + [object], + ), + returnValueForMissingStub: null, + ); + @override + void writeAll( + Iterable? objects, [ + String? separator = r'', + ]) => + super.noSuchMethod( + Invocation.method( + #writeAll, + [ + objects, + separator, + ], + ), + returnValueForMissingStub: null, + ); + @override + void writeln([Object? object = r'']) => super.noSuchMethod( + Invocation.method( + #writeln, + [object], + ), + returnValueForMissingStub: null, + ); + @override + void writeCharCode(int? charCode) => super.noSuchMethod( + Invocation.method( + #writeCharCode, + [charCode], + ), + returnValueForMissingStub: null, + ); + @override + void addError( + Object? error, [ + StackTrace? stackTrace, + ]) => + super.noSuchMethod( + Invocation.method( + #addError, + [ + error, + stackTrace, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future addStream(_i8.Stream>? stream) => + (super.noSuchMethod( + Invocation.method( + #addStream, + [stream], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future flush() => (super.noSuchMethod( + Invocation.method( + #flush, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [HttpClientResponse]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpClientResponse extends _i1.Mock + implements _i11.HttpClientResponse { + @override + int get statusCode => (super.noSuchMethod( + Invocation.getter(#statusCode), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + String get reasonPhrase => (super.noSuchMethod( + Invocation.getter(#reasonPhrase), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + int get contentLength => (super.noSuchMethod( + Invocation.getter(#contentLength), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + _i10.HttpClientResponseCompressionState get compressionState => + (super.noSuchMethod( + Invocation.getter(#compressionState), + returnValue: _i10.HttpClientResponseCompressionState.notCompressed, + returnValueForMissingStub: + _i10.HttpClientResponseCompressionState.notCompressed, + ) as _i10.HttpClientResponseCompressionState); + @override + bool get persistentConnection => (super.noSuchMethod( + Invocation.getter(#persistentConnection), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + bool get isRedirect => (super.noSuchMethod( + Invocation.getter(#isRedirect), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + List<_i10.RedirectInfo> get redirects => (super.noSuchMethod( + Invocation.getter(#redirects), + returnValue: <_i10.RedirectInfo>[], + returnValueForMissingStub: <_i10.RedirectInfo>[], + ) as List<_i10.RedirectInfo>); + @override + _i10.HttpHeaders get headers => (super.noSuchMethod( + Invocation.getter(#headers), + returnValue: _FakeHttpHeaders_17( + this, + Invocation.getter(#headers), + ), + returnValueForMissingStub: _FakeHttpHeaders_17( + this, + Invocation.getter(#headers), + ), + ) as _i10.HttpHeaders); + @override + List<_i10.Cookie> get cookies => (super.noSuchMethod( + Invocation.getter(#cookies), + returnValue: <_i10.Cookie>[], + returnValueForMissingStub: <_i10.Cookie>[], + ) as List<_i10.Cookie>); + @override + bool get isBroadcast => (super.noSuchMethod( + Invocation.getter(#isBroadcast), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i8.Future get length => (super.noSuchMethod( + Invocation.getter(#length), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future get isEmpty => (super.noSuchMethod( + Invocation.getter(#isEmpty), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future> get first => (super.noSuchMethod( + Invocation.getter(#first), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future> get last => (super.noSuchMethod( + Invocation.getter(#last), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future> get single => (super.noSuchMethod( + Invocation.getter(#single), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future<_i11.HttpClientResponse> redirect([ + String? method, + Uri? url, + bool? followLoops, + ]) => + (super.noSuchMethod( + Invocation.method( + #redirect, + [ + method, + url, + followLoops, + ], + ), + returnValue: _i8.Future<_i11.HttpClientResponse>.value( + _FakeHttpClientResponse_18( + this, + Invocation.method( + #redirect, + [ + method, + url, + followLoops, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i11.HttpClientResponse>.value( + _FakeHttpClientResponse_18( + this, + Invocation.method( + #redirect, + [ + method, + url, + followLoops, + ], + ), + )), + ) as _i8.Future<_i11.HttpClientResponse>); + @override + _i8.Future<_i10.Socket> detachSocket() => (super.noSuchMethod( + Invocation.method( + #detachSocket, + [], + ), + returnValue: _i8.Future<_i10.Socket>.value(_FakeSocket_20( + this, + Invocation.method( + #detachSocket, + [], + ), + )), + returnValueForMissingStub: _i8.Future<_i10.Socket>.value(_FakeSocket_20( + this, + Invocation.method( + #detachSocket, + [], + ), + )), + ) as _i8.Future<_i10.Socket>); + @override + _i8.Stream> asBroadcastStream({ + void Function(_i8.StreamSubscription>)? onListen, + void Function(_i8.StreamSubscription>)? onCancel, + }) => + (super.noSuchMethod( + Invocation.method( + #asBroadcastStream, + [], + { + #onListen: onListen, + #onCancel: onCancel, + }, + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.StreamSubscription> listen( + void Function(List)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) => + (super.noSuchMethod( + Invocation.method( + #listen, + [onData], + { + #onError: onError, + #onDone: onDone, + #cancelOnError: cancelOnError, + }, + ), + returnValue: _FakeStreamSubscription_21>( + this, + Invocation.method( + #listen, + [onData], + { + #onError: onError, + #onDone: onDone, + #cancelOnError: cancelOnError, + }, + ), + ), + returnValueForMissingStub: _FakeStreamSubscription_21>( + this, + Invocation.method( + #listen, + [onData], + { + #onError: onError, + #onDone: onDone, + #cancelOnError: cancelOnError, + }, + ), + ), + ) as _i8.StreamSubscription>); + @override + _i8.Stream> where(bool Function(List)? test) => + (super.noSuchMethod( + Invocation.method( + #where, + [test], + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Stream map(S Function(List)? convert) => (super.noSuchMethod( + Invocation.method( + #map, + [convert], + ), + returnValue: _i8.Stream.empty(), + returnValueForMissingStub: _i8.Stream.empty(), + ) as _i8.Stream); + @override + _i8.Stream asyncMap(_i8.FutureOr Function(List)? convert) => + (super.noSuchMethod( + Invocation.method( + #asyncMap, + [convert], + ), + returnValue: _i8.Stream.empty(), + returnValueForMissingStub: _i8.Stream.empty(), + ) as _i8.Stream); + @override + _i8.Stream asyncExpand(_i8.Stream? Function(List)? convert) => + (super.noSuchMethod( + Invocation.method( + #asyncExpand, + [convert], + ), + returnValue: _i8.Stream.empty(), + returnValueForMissingStub: _i8.Stream.empty(), + ) as _i8.Stream); + @override + _i8.Stream> handleError( + Function? onError, { + bool Function(dynamic)? test, + }) => + (super.noSuchMethod( + Invocation.method( + #handleError, + [onError], + {#test: test}, + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Stream expand(Iterable Function(List)? convert) => + (super.noSuchMethod( + Invocation.method( + #expand, + [convert], + ), + returnValue: _i8.Stream.empty(), + returnValueForMissingStub: _i8.Stream.empty(), + ) as _i8.Stream); + @override + _i8.Future pipe(_i8.StreamConsumer>? streamConsumer) => + (super.noSuchMethod( + Invocation.method( + #pipe, + [streamConsumer], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Stream transform( + _i8.StreamTransformer, S>? streamTransformer) => + (super.noSuchMethod( + Invocation.method( + #transform, + [streamTransformer], + ), + returnValue: _i8.Stream.empty(), + returnValueForMissingStub: _i8.Stream.empty(), + ) as _i8.Stream); + @override + _i8.Future> reduce( + List Function( + List, + List, + )? combine) => + (super.noSuchMethod( + Invocation.method( + #reduce, + [combine], + ), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future fold( + S? initialValue, + S Function( + S, + List, + )? combine, + ) => + (super.noSuchMethod( + Invocation.method( + #fold, + [ + initialValue, + combine, + ], + ), + returnValue: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #fold, + [ + initialValue, + combine, + ], + ), + ), + (S v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #fold, + [ + initialValue, + combine, + ], + ), + ), + returnValueForMissingStub: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #fold, + [ + initialValue, + combine, + ], + ), + ), + (S v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #fold, + [ + initialValue, + combine, + ], + ), + ), + ) as _i8.Future); + @override + _i8.Future join([String? separator = r'']) => (super.noSuchMethod( + Invocation.method( + #join, + [separator], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i8.Future contains(Object? needle) => (super.noSuchMethod( + Invocation.method( + #contains, + [needle], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future forEach(void Function(List)? action) => + (super.noSuchMethod( + Invocation.method( + #forEach, + [action], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future every(bool Function(List)? test) => (super.noSuchMethod( + Invocation.method( + #every, + [test], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future any(bool Function(List)? test) => (super.noSuchMethod( + Invocation.method( + #any, + [test], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Stream cast() => (super.noSuchMethod( + Invocation.method( + #cast, + [], + ), + returnValue: _i8.Stream.empty(), + returnValueForMissingStub: _i8.Stream.empty(), + ) as _i8.Stream); + @override + _i8.Future>> toList() => (super.noSuchMethod( + Invocation.method( + #toList, + [], + ), + returnValue: _i8.Future>>.value(>[]), + returnValueForMissingStub: + _i8.Future>>.value(>[]), + ) as _i8.Future>>); + @override + _i8.Future>> toSet() => (super.noSuchMethod( + Invocation.method( + #toSet, + [], + ), + returnValue: _i8.Future>>.value(>{}), + returnValueForMissingStub: + _i8.Future>>.value(>{}), + ) as _i8.Future>>); + @override + _i8.Future drain([E? futureValue]) => (super.noSuchMethod( + Invocation.method( + #drain, + [futureValue], + ), + returnValue: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #drain, + [futureValue], + ), + ), + (E v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #drain, + [futureValue], + ), + ), + returnValueForMissingStub: _i67.ifNotNull( + _i67.dummyValueOrNull( + this, + Invocation.method( + #drain, + [futureValue], + ), + ), + (E v) => _i8.Future.value(v), + ) ?? + _FakeFuture_6( + this, + Invocation.method( + #drain, + [futureValue], + ), + ), + ) as _i8.Future); + @override + _i8.Stream> take(int? count) => (super.noSuchMethod( + Invocation.method( + #take, + [count], + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Stream> takeWhile(bool Function(List)? test) => + (super.noSuchMethod( + Invocation.method( + #takeWhile, + [test], + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Stream> skip(int? count) => (super.noSuchMethod( + Invocation.method( + #skip, + [count], + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Stream> skipWhile(bool Function(List)? test) => + (super.noSuchMethod( + Invocation.method( + #skipWhile, + [test], + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Stream> distinct( + [bool Function( + List, + List, + )? equals]) => + (super.noSuchMethod( + Invocation.method( + #distinct, + [equals], + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); + @override + _i8.Future> firstWhere( + bool Function(List)? test, { + List Function()? orElse, + }) => + (super.noSuchMethod( + Invocation.method( + #firstWhere, + [test], + {#orElse: orElse}, + ), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future> lastWhere( + bool Function(List)? test, { + List Function()? orElse, + }) => + (super.noSuchMethod( + Invocation.method( + #lastWhere, + [test], + {#orElse: orElse}, + ), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future> singleWhere( + bool Function(List)? test, { + List Function()? orElse, + }) => + (super.noSuchMethod( + Invocation.method( + #singleWhere, + [test], + {#orElse: orElse}, + ), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Future> elementAt(int? index) => (super.noSuchMethod( + Invocation.method( + #elementAt, + [index], + ), + returnValue: _i8.Future>.value([]), + returnValueForMissingStub: _i8.Future>.value([]), + ) as _i8.Future>); + @override + _i8.Stream> timeout( + Duration? timeLimit, { + void Function(_i8.EventSink>)? onTimeout, + }) => + (super.noSuchMethod( + Invocation.method( + #timeout, + [timeLimit], + {#onTimeout: onTimeout}, + ), + returnValue: _i8.Stream>.empty(), + returnValueForMissingStub: _i8.Stream>.empty(), + ) as _i8.Stream>); +} + +/// A class which mocks [HttpHeaders]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpHeaders extends _i1.Mock implements _i10.HttpHeaders { + @override + set date(DateTime? _date) => super.noSuchMethod( + Invocation.setter( + #date, + _date, + ), + returnValueForMissingStub: null, + ); + @override + set expires(DateTime? _expires) => super.noSuchMethod( + Invocation.setter( + #expires, + _expires, + ), + returnValueForMissingStub: null, + ); + @override + set ifModifiedSince(DateTime? _ifModifiedSince) => super.noSuchMethod( + Invocation.setter( + #ifModifiedSince, + _ifModifiedSince, + ), + returnValueForMissingStub: null, + ); + @override + set host(String? _host) => super.noSuchMethod( + Invocation.setter( + #host, + _host, + ), + returnValueForMissingStub: null, + ); + @override + set port(int? _port) => super.noSuchMethod( + Invocation.setter( + #port, + _port, + ), + returnValueForMissingStub: null, + ); + @override + set contentType(_i10.ContentType? _contentType) => super.noSuchMethod( + Invocation.setter( + #contentType, + _contentType, + ), + returnValueForMissingStub: null, + ); + @override + int get contentLength => (super.noSuchMethod( + Invocation.getter(#contentLength), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set contentLength(int? _contentLength) => super.noSuchMethod( + Invocation.setter( + #contentLength, + _contentLength, + ), + returnValueForMissingStub: null, + ); + @override + bool get persistentConnection => (super.noSuchMethod( + Invocation.getter(#persistentConnection), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set persistentConnection(bool? _persistentConnection) => super.noSuchMethod( + Invocation.setter( + #persistentConnection, + _persistentConnection, + ), + returnValueForMissingStub: null, + ); + @override + bool get chunkedTransferEncoding => (super.noSuchMethod( + Invocation.getter(#chunkedTransferEncoding), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + set chunkedTransferEncoding(bool? _chunkedTransferEncoding) => + super.noSuchMethod( + Invocation.setter( + #chunkedTransferEncoding, + _chunkedTransferEncoding, + ), + returnValueForMissingStub: null, + ); + @override + List? operator [](String? name) => (super.noSuchMethod( + Invocation.method( + #[], + [name], + ), + returnValueForMissingStub: null, + ) as List?); + @override + String? value(String? name) => (super.noSuchMethod( + Invocation.method( + #value, + [name], + ), + returnValueForMissingStub: null, + ) as String?); + @override + void add( + String? name, + Object? value, { + bool? preserveHeaderCase = false, + }) => + super.noSuchMethod( + Invocation.method( + #add, + [ + name, + value, + ], + {#preserveHeaderCase: preserveHeaderCase}, + ), + returnValueForMissingStub: null, + ); + @override + void set( + String? name, + Object? value, { + bool? preserveHeaderCase = false, + }) => + super.noSuchMethod( + Invocation.method( + #set, + [ + name, + value, + ], + {#preserveHeaderCase: preserveHeaderCase}, + ), + returnValueForMissingStub: null, + ); + @override + void remove( + String? name, + Object? value, + ) => + super.noSuchMethod( + Invocation.method( + #remove, + [ + name, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeAll(String? name) => super.noSuchMethod( + Invocation.method( + #removeAll, + [name], + ), + returnValueForMissingStub: null, + ); + @override + void forEach( + void Function( + String, + List, + )? action) => + super.noSuchMethod( + Invocation.method( + #forEach, + [action], + ), + returnValueForMissingStub: null, + ); + @override + void noFolding(String? name) => super.noSuchMethod( + Invocation.method( + #noFolding, + [name], + ), + returnValueForMissingStub: null, + ); + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [InboxApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInboxApi extends _i1.Mock implements _i73.InboxApi { + @override + _i8.Future?> getConversations({ + String? scope = null, + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getConversations, + [], + { + #scope: scope, + #forceRefresh: forceRefresh, + }, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i65.Conversation?> getConversation( + String? id, { + bool? refresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getConversation, + [id], + {#refresh: refresh}, + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); + @override + _i8.Future<_i33.UnreadCount?> getUnreadCount() => (super.noSuchMethod( + Invocation.method( + #getUnreadCount, + [], + ), + returnValue: _i8.Future<_i33.UnreadCount?>.value(), + returnValueForMissingStub: _i8.Future<_i33.UnreadCount?>.value(), + ) as _i8.Future<_i33.UnreadCount?>); + @override + _i8.Future<_i65.Conversation?> addMessage( + String? conversationId, + String? body, + List? recipientIds, + List? attachmentIds, + List? includeMessageIds, + ) => + (super.noSuchMethod( + Invocation.method( + #addMessage, + [ + conversationId, + body, + recipientIds, + attachmentIds, + includeMessageIds, + ], + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); + @override + _i8.Future?> getRecipients( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getRecipients, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i65.Conversation?> createConversation( + String? courseId, + List? recipientIds, + String? subject, + String? body, + List? attachmentIds, + ) => + (super.noSuchMethod( + Invocation.method( + #createConversation, + [ + courseId, + recipientIds, + subject, + body, + attachmentIds, + ], + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); +} + +/// A class which mocks [NavigatorObserver]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNavigatorObserver extends _i1.Mock implements _i17.NavigatorObserver { + @override + void didPush( + _i17.Route? route, + _i17.Route? previousRoute, + ) => + super.noSuchMethod( + Invocation.method( + #didPush, + [ + route, + previousRoute, + ], + ), + returnValueForMissingStub: null, + ); + @override + void didPop( + _i17.Route? route, + _i17.Route? previousRoute, + ) => + super.noSuchMethod( + Invocation.method( + #didPop, + [ + route, + previousRoute, + ], + ), + returnValueForMissingStub: null, + ); + @override + void didRemove( + _i17.Route? route, + _i17.Route? previousRoute, + ) => + super.noSuchMethod( + Invocation.method( + #didRemove, + [ + route, + previousRoute, + ], + ), + returnValueForMissingStub: null, + ); + @override + void didReplace({ + _i17.Route? newRoute, + _i17.Route? oldRoute, + }) => + super.noSuchMethod( + Invocation.method( + #didReplace, + [], + { + #newRoute: newRoute, + #oldRoute: oldRoute, + }, + ), + returnValueForMissingStub: null, + ); + @override + void didStartUserGesture( + _i17.Route? route, + _i17.Route? previousRoute, + ) => + super.noSuchMethod( + Invocation.method( + #didStartUserGesture, + [ + route, + previousRoute, + ], + ), + returnValueForMissingStub: null, + ); + @override + void didStopUserGesture() => super.noSuchMethod( + Invocation.method( + #didStopUserGesture, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [NotificationUtil]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNotificationUtil extends _i1.Mock implements _i75.NotificationUtil { + @override + _i8.Future scheduleReminder( + _i41.AppLocalizations? l10n, + String? title, + String? body, + _i40.Reminder? reminder, + ) => + (super.noSuchMethod( + Invocation.method( + #scheduleReminder, + [ + l10n, + title, + body, + reminder, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteNotification(int? id) => (super.noSuchMethod( + Invocation.method( + #deleteNotification, + [id], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteNotifications(List? ids) => (super.noSuchMethod( + Invocation.method( + #deleteNotifications, + [ids], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [OAuthApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockOAuthApi extends _i1.Mock implements _i76.OAuthApi { + @override + _i8.Future<_i77.AuthenticatedUrl?> getAuthenticatedUrl(String? targetUrl) => + (super.noSuchMethod( + Invocation.method( + #getAuthenticatedUrl, + [targetUrl], + ), + returnValue: _i8.Future<_i77.AuthenticatedUrl?>.value(), + returnValueForMissingStub: _i8.Future<_i77.AuthenticatedUrl?>.value(), + ) as _i8.Future<_i77.AuthenticatedUrl?>); +} + +/// A class which mocks [PairingInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPairingInteractor extends _i1.Mock implements _i78.PairingInteractor { + @override + _i8.Future<_i13.QRPairingScanResult> scanQRCode() => (super.noSuchMethod( + Invocation.method( + #scanQRCode, + [], + ), + returnValue: _i8.Future<_i13.QRPairingScanResult>.value( + _FakeQRPairingScanResult_22( + this, + Invocation.method( + #scanQRCode, + [], + ), + )), + returnValueForMissingStub: _i8.Future<_i13.QRPairingScanResult>.value( + _FakeQRPairingScanResult_22( + this, + Invocation.method( + #scanQRCode, + [], + ), + )), + ) as _i8.Future<_i13.QRPairingScanResult>); + @override + _i8.Future pairWithStudent(String? pairingCode) => (super.noSuchMethod( + Invocation.method( + #pairWithStudent, + [pairingCode], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [PageApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPageApi extends _i1.Mock implements _i79.PageApi { + @override + _i8.Future<_i59.CanvasPage?> getCourseFrontPage( + String? courseId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getCourseFrontPage, + [courseId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i59.CanvasPage?>.value(), + returnValueForMissingStub: _i8.Future<_i59.CanvasPage?>.value(), + ) as _i8.Future<_i59.CanvasPage?>); +} + +/// A class which mocks [FlutterLocalNotificationsPlugin]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterLocalNotificationsPlugin extends _i1.Mock + implements _i80.FlutterLocalNotificationsPlugin { + @override + _i8.Future initialize( + _i80.InitializationSettings? initializationSettings, { + _i80.DidReceiveNotificationResponseCallback? + onDidReceiveNotificationResponse, + _i80.DidReceiveBackgroundNotificationResponseCallback? + onDidReceiveBackgroundNotificationResponse, + }) => + (super.noSuchMethod( + Invocation.method( + #initialize, + [initializationSettings], + { + #onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + #onDidReceiveBackgroundNotificationResponse: + onDidReceiveBackgroundNotificationResponse, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future<_i80.NotificationAppLaunchDetails?> + getNotificationAppLaunchDetails() => (super.noSuchMethod( + Invocation.method( + #getNotificationAppLaunchDetails, + [], + ), + returnValue: _i8.Future<_i80.NotificationAppLaunchDetails?>.value(), + returnValueForMissingStub: + _i8.Future<_i80.NotificationAppLaunchDetails?>.value(), + ) as _i8.Future<_i80.NotificationAppLaunchDetails?>); + @override + _i8.Future show( + int? id, + String? title, + String? body, + _i80.NotificationDetails? notificationDetails, { + String? payload, + }) => + (super.noSuchMethod( + Invocation.method( + #show, + [ + id, + title, + body, + notificationDetails, + ], + {#payload: payload}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future cancel( + int? id, { + String? tag, + }) => + (super.noSuchMethod( + Invocation.method( + #cancel, + [id], + {#tag: tag}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future cancelAll() => (super.noSuchMethod( + Invocation.method( + #cancelAll, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future zonedSchedule( + int? id, + String? title, + String? body, + _i81.TZDateTime? scheduledDate, + _i80.NotificationDetails? notificationDetails, { + required _i80.UILocalNotificationDateInterpretation? + uiLocalNotificationDateInterpretation, + bool? androidAllowWhileIdle = false, + _i80.AndroidScheduleMode? androidScheduleMode, + String? payload, + _i80.DateTimeComponents? matchDateTimeComponents, + }) => + (super.noSuchMethod( + Invocation.method( + #zonedSchedule, + [ + id, + title, + body, + scheduledDate, + notificationDetails, + ], + { + #uiLocalNotificationDateInterpretation: + uiLocalNotificationDateInterpretation, + #androidAllowWhileIdle: androidAllowWhileIdle, + #androidScheduleMode: androidScheduleMode, + #payload: payload, + #matchDateTimeComponents: matchDateTimeComponents, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future periodicallyShow( + int? id, + String? title, + String? body, + _i80.RepeatInterval? repeatInterval, + _i80.NotificationDetails? notificationDetails, { + String? payload, + bool? androidAllowWhileIdle = false, + _i80.AndroidScheduleMode? androidScheduleMode, + }) => + (super.noSuchMethod( + Invocation.method( + #periodicallyShow, + [ + id, + title, + body, + repeatInterval, + notificationDetails, + ], + { + #payload: payload, + #androidAllowWhileIdle: androidAllowWhileIdle, + #androidScheduleMode: androidScheduleMode, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future> + pendingNotificationRequests() => (super.noSuchMethod( + Invocation.method( + #pendingNotificationRequests, + [], + ), + returnValue: + _i8.Future>.value( + <_i80.PendingNotificationRequest>[]), + returnValueForMissingStub: + _i8.Future>.value( + <_i80.PendingNotificationRequest>[]), + ) as _i8.Future>); + @override + _i8.Future> getActiveNotifications() => + (super.noSuchMethod( + Invocation.method( + #getActiveNotifications, + [], + ), + returnValue: _i8.Future>.value( + <_i80.ActiveNotification>[]), + returnValueForMissingStub: + _i8.Future>.value( + <_i80.ActiveNotification>[]), + ) as _i8.Future>); +} + +/// A class which mocks [PairingUtil]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPairingUtil extends _i1.Mock implements _i82.PairingUtil { + @override + dynamic pairNewStudent( + _i17.BuildContext? context, + dynamic Function()? onSuccess, + ) => + super.noSuchMethod( + Invocation.method( + #pairNewStudent, + [ + context, + onSuccess, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [QuickNav]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockQuickNav extends _i1.Mock implements _i83.QuickNav { + @override + _i8.Future push( + _i17.BuildContext? context, + _i17.Widget? widget, + ) => + (super.noSuchMethod( + Invocation.method( + #push, + [ + context, + widget, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future pushRoute( + _i17.BuildContext? context, + String? route, { + _i84.TransitionType? transitionType = _i84.TransitionType.material, + }) => + (super.noSuchMethod( + Invocation.method( + #pushRoute, + [ + context, + route, + ], + {#transitionType: transitionType}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future replaceRoute( + _i17.BuildContext? context, + String? route, { + _i84.TransitionType? transitionType = _i84.TransitionType.material, + }) => + (super.noSuchMethod( + Invocation.method( + #replaceRoute, + [ + context, + route, + ], + {#transitionType: transitionType}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future pushRouteAndClearStack( + _i17.BuildContext? context, + String? route, { + _i84.TransitionType? transitionType = _i84.TransitionType.material, + }) => + (super.noSuchMethod( + Invocation.method( + #pushRouteAndClearStack, + [ + context, + route, + ], + {#transitionType: transitionType}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future pushRouteWithCustomTransition( + _i17.BuildContext? context, + String? route, + bool? clearStack, + Duration? transitionDuration, + _i17.RouteTransitionsBuilder? transitionsBuilder, { + _i84.TransitionType? transitionType = _i84.TransitionType.custom, + }) => + (super.noSuchMethod( + Invocation.method( + #pushRouteWithCustomTransition, + [ + context, + route, + clearStack, + transitionDuration, + transitionsBuilder, + ], + {#transitionType: transitionType}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future routeInternally( + _i17.BuildContext? context, + String? url, + ) => + (super.noSuchMethod( + Invocation.method( + #routeInternally, + [ + context, + url, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future showDialog({ + required _i17.BuildContext? context, + bool? barrierDismissible = true, + required _i17.WidgetBuilder? builder, + bool? useRootNavigator = true, + _i17.RouteSettings? routeSettings, + }) => + (super.noSuchMethod( + Invocation.method( + #showDialog, + [], + { + #context: context, + #barrierDismissible: barrierDismissible, + #builder: builder, + #useRootNavigator: useRootNavigator, + #routeSettings: routeSettings, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [ReminderDb]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockReminderDb extends _i1.Mock implements _i85.ReminderDb { + @override + _i5.Database get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeDatabase_3( + this, + Invocation.getter(#db), + ), + returnValueForMissingStub: _FakeDatabase_3( + this, + Invocation.getter(#db), + ), + ) as _i5.Database); + @override + set db(_i5.Database? _db) => super.noSuchMethod( + Invocation.setter( + #db, + _db, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i40.Reminder?> insert(_i40.Reminder? data) => (super.noSuchMethod( + Invocation.method( + #insert, + [data], + ), + returnValue: _i8.Future<_i40.Reminder?>.value(), + returnValueForMissingStub: _i8.Future<_i40.Reminder?>.value(), + ) as _i8.Future<_i40.Reminder?>); + @override + _i8.Future<_i40.Reminder?> getById(int? id) => (super.noSuchMethod( + Invocation.method( + #getById, + [id], + ), + returnValue: _i8.Future<_i40.Reminder?>.value(), + returnValueForMissingStub: _i8.Future<_i40.Reminder?>.value(), + ) as _i8.Future<_i40.Reminder?>); + @override + _i8.Future<_i40.Reminder?> getByItem( + String? userDomain, + String? userId, + String? type, + String? itemId, + ) => + (super.noSuchMethod( + Invocation.method( + #getByItem, + [ + userDomain, + userId, + type, + itemId, + ], + ), + returnValue: _i8.Future<_i40.Reminder?>.value(), + returnValueForMissingStub: _i8.Future<_i40.Reminder?>.value(), + ) as _i8.Future<_i40.Reminder?>); + @override + _i8.Future deleteById(int? id) => (super.noSuchMethod( + Invocation.method( + #deleteById, + [id], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future deleteAllForUser( + String? userDomain, + String? userId, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteAllForUser, + [ + userDomain, + userId, + ], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [FirebaseRemoteConfig]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseRemoteConfig extends _i1.Mock + implements _i86.FirebaseRemoteConfig { + @override + _i9.FirebaseApp get app => (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_13( + this, + Invocation.getter(#app), + ), + returnValueForMissingStub: _FakeFirebaseApp_13( + this, + Invocation.getter(#app), + ), + ) as _i9.FirebaseApp); + @override + DateTime get lastFetchTime => (super.noSuchMethod( + Invocation.getter(#lastFetchTime), + returnValue: _FakeDateTime_23( + this, + Invocation.getter(#lastFetchTime), + ), + returnValueForMissingStub: _FakeDateTime_23( + this, + Invocation.getter(#lastFetchTime), + ), + ) as DateTime); + @override + _i14.RemoteConfigFetchStatus get lastFetchStatus => (super.noSuchMethod( + Invocation.getter(#lastFetchStatus), + returnValue: _i14.RemoteConfigFetchStatus.noFetchYet, + returnValueForMissingStub: _i14.RemoteConfigFetchStatus.noFetchYet, + ) as _i14.RemoteConfigFetchStatus); + @override + _i14.RemoteConfigSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeRemoteConfigSettings_24( + this, + Invocation.getter(#settings), + ), + returnValueForMissingStub: _FakeRemoteConfigSettings_24( + this, + Invocation.getter(#settings), + ), + ) as _i14.RemoteConfigSettings); + @override + _i8.Stream<_i14.RemoteConfigUpdate> get onConfigUpdated => + (super.noSuchMethod( + Invocation.getter(#onConfigUpdated), + returnValue: _i8.Stream<_i14.RemoteConfigUpdate>.empty(), + returnValueForMissingStub: _i8.Stream<_i14.RemoteConfigUpdate>.empty(), + ) as _i8.Stream<_i14.RemoteConfigUpdate>); + @override + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + @override + _i8.Future activate() => (super.noSuchMethod( + Invocation.method( + #activate, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future ensureInitialized() => (super.noSuchMethod( + Invocation.method( + #ensureInitialized, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future fetch() => (super.noSuchMethod( + Invocation.method( + #fetch, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future fetchAndActivate() => (super.noSuchMethod( + Invocation.method( + #fetchAndActivate, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + Map getAll() => (super.noSuchMethod( + Invocation.method( + #getAll, + [], + ), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + @override + bool getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + int getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + double getDouble(String? key) => (super.noSuchMethod( + Invocation.method( + #getDouble, + [key], + ), + returnValue: 0.0, + returnValueForMissingStub: 0.0, + ) as double); + @override + String getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + _i14.RemoteConfigValue getValue(String? key) => (super.noSuchMethod( + Invocation.method( + #getValue, + [key], + ), + returnValue: _FakeRemoteConfigValue_25( + this, + Invocation.method( + #getValue, + [key], + ), + ), + returnValueForMissingStub: _FakeRemoteConfigValue_25( + this, + Invocation.method( + #getValue, + [key], + ), + ), + ) as _i14.RemoteConfigValue); + @override + _i8.Future setConfigSettings( + _i14.RemoteConfigSettings? remoteConfigSettings) => + (super.noSuchMethod( + Invocation.method( + #setConfigSettings, + [remoteConfigSettings], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setDefaults(Map? defaultParameters) => + (super.noSuchMethod( + Invocation.method( + #setDefaults, + [defaultParameters], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [FlutterSnackbarVeneer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterSnackbarVeneer extends _i1.Mock + implements _i87.FlutterSnackbarVeneer { + @override + dynamic showSnackBar( + dynamic context, + String? message, + ) => + super.noSuchMethod( + Invocation.method( + #showSnackBar, + [ + context, + message, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [StudentAddedNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockStudentAddedNotifier extends _i1.Mock + implements _i82.StudentAddedNotifier { + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + void notify() => super.noSuchMethod( + Invocation.method( + #notify, + [], + ), + returnValueForMissingStub: null, + ); + @override + void addListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [UrlLauncher]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUrlLauncher extends _i1.Mock implements _i88.UrlLauncher { + @override + _i15.MethodChannel get channel => (super.noSuchMethod( + Invocation.getter(#channel), + returnValue: _FakeMethodChannel_26( + this, + Invocation.getter(#channel), + ), + returnValueForMissingStub: _FakeMethodChannel_26( + this, + Invocation.getter(#channel), + ), + ) as _i15.MethodChannel); + @override + set channel(_i15.MethodChannel? _channel) => super.noSuchMethod( + Invocation.setter( + #channel, + _channel, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future canLaunch( + String? url, { + bool? excludeInstructure = true, + }) => + (super.noSuchMethod( + Invocation.method( + #canLaunch, + [url], + {#excludeInstructure: excludeInstructure}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future launch( + String? url, { + bool? excludeInstructure = true, + }) => + (super.noSuchMethod( + Invocation.method( + #launch, + [url], + {#excludeInstructure: excludeInstructure}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future launchAppStore() => (super.noSuchMethod( + Invocation.method( + #launchAppStore, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [UserColorsDb]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUserColorsDb extends _i1.Mock implements _i89.UserColorsDb { + @override + _i5.Database get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeDatabase_3( + this, + Invocation.getter(#db), + ), + returnValueForMissingStub: _FakeDatabase_3( + this, + Invocation.getter(#db), + ), + ) as _i5.Database); + @override + set db(_i5.Database? _db) => super.noSuchMethod( + Invocation.setter( + #db, + _db, + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i90.UserColor?> getById(int? id) => (super.noSuchMethod( + Invocation.method( + #getById, + [id], + ), + returnValue: _i8.Future<_i90.UserColor?>.value(), + returnValueForMissingStub: _i8.Future<_i90.UserColor?>.value(), + ) as _i8.Future<_i90.UserColor?>); + @override + _i8.Future insertOrUpdateAll( + String? domain, + String? userId, + _i91.UserColors? colors, + ) => + (super.noSuchMethod( + Invocation.method( + #insertOrUpdateAll, + [ + domain, + userId, + colors, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future<_i90.UserColor?> insertOrUpdate(_i90.UserColor? data) => + (super.noSuchMethod( + Invocation.method( + #insertOrUpdate, + [data], + ), + returnValue: _i8.Future<_i90.UserColor?>.value(), + returnValueForMissingStub: _i8.Future<_i90.UserColor?>.value(), + ) as _i8.Future<_i90.UserColor?>); + @override + _i8.Future<_i90.UserColor?> getByContext( + String? userDomain, + String? userId, + String? canvasContext, + ) => + (super.noSuchMethod( + Invocation.method( + #getByContext, + [ + userDomain, + userId, + canvasContext, + ], + ), + returnValue: _i8.Future<_i90.UserColor?>.value(), + returnValueForMissingStub: _i8.Future<_i90.UserColor?>.value(), + ) as _i8.Future<_i90.UserColor?>); + @override + _i8.Future deleteById(int? id) => (super.noSuchMethod( + Invocation.method( + #deleteById, + [id], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future deleteAllForUser( + String? userDomain, + String? userId, + ) => + (super.noSuchMethod( + Invocation.method( + #deleteAllForUser, + [ + userDomain, + userId, + ], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [WebLoginInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebLoginInteractor extends _i1.Mock + implements _i92.WebLoginInteractor { + @override + _i8.Future<_i44.MobileVerifyResult?> mobileVerify(String? domain) => + (super.noSuchMethod( + Invocation.method( + #mobileVerify, + [domain], + ), + returnValue: _i8.Future<_i44.MobileVerifyResult?>.value(), + returnValueForMissingStub: _i8.Future<_i44.MobileVerifyResult?>.value(), + ) as _i8.Future<_i44.MobileVerifyResult?>); + @override + _i8.Future performLogin( + _i44.MobileVerifyResult? result, + String? oAuthRequest, + ) => + (super.noSuchMethod( + Invocation.method( + #performLogin, + [ + result, + oAuthRequest, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [WebContentInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebContentInteractor extends _i1.Mock + implements _i93.WebContentInteractor { + @override + _i8.Future getAuthUrl(String? targetUrl) => (super.noSuchMethod( + Invocation.method( + #getAuthUrl, + [targetUrl], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i16.JavascriptChannel ltiToolPressedChannel( + _i16.JavascriptMessageHandler? handler) => + (super.noSuchMethod( + Invocation.method( + #ltiToolPressedChannel, + [handler], + ), + returnValue: _FakeJavascriptChannel_27( + this, + Invocation.method( + #ltiToolPressedChannel, + [handler], + ), + ), + returnValueForMissingStub: _FakeJavascriptChannel_27( + this, + Invocation.method( + #ltiToolPressedChannel, + [handler], + ), + ), + ) as _i16.JavascriptChannel); +} + +/// A class which mocks [AlertThresholdsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAlertThresholdsInteractor extends _i1.Mock + implements _i94.AlertThresholdsInteractor { + @override + _i8.Future?> getAlertThresholdsForStudent( + String? studentId, { + bool? forceRefresh = true, + }) => + (super.noSuchMethod( + Invocation.method( + #getAlertThresholdsForStudent, + [studentId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: + _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i34.AlertThreshold?> updateAlertThreshold( + _i32.AlertType? type, + String? studentId, + _i34.AlertThreshold? threshold, { + String? value, + }) => + (super.noSuchMethod( + Invocation.method( + #updateAlertThreshold, + [ + type, + studentId, + threshold, + ], + {#value: value}, + ), + returnValue: _i8.Future<_i34.AlertThreshold?>.value(), + returnValueForMissingStub: _i8.Future<_i34.AlertThreshold?>.value(), + ) as _i8.Future<_i34.AlertThreshold?>); + @override + _i8.Future deleteStudent(String? studentId) => (super.noSuchMethod( + Invocation.method( + #deleteStudent, + [studentId], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future canDeleteStudent(String? studentId) => (super.noSuchMethod( + Invocation.method( + #canDeleteStudent, + [studentId], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); +} + +/// A class which mocks [AlertsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAlertsInteractor extends _i1.Mock implements _i95.AlertsInteractor { + @override + _i8.Future<_i95.AlertsList?> getAlertsForStudent( + String? studentId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getAlertsForStudent, + [ + studentId, + forceRefresh, + ], + ), + returnValue: _i8.Future<_i95.AlertsList?>.value(), + returnValueForMissingStub: _i8.Future<_i95.AlertsList?>.value(), + ) as _i8.Future<_i95.AlertsList?>); + @override + _i8.Future<_i32.Alert?> markAlertRead( + String? studentId, + String? alertId, + ) => + (super.noSuchMethod( + Invocation.method( + #markAlertRead, + [ + studentId, + alertId, + ], + ), + returnValue: _i8.Future<_i32.Alert?>.value(), + returnValueForMissingStub: _i8.Future<_i32.Alert?>.value(), + ) as _i8.Future<_i32.Alert?>); + @override + _i8.Future markAlertDismissed( + String? studentId, + String? alertId, + ) => + (super.noSuchMethod( + Invocation.method( + #markAlertDismissed, + [ + studentId, + alertId, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [AnnouncementDetailsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAnnouncementDetailsInteractor extends _i1.Mock + implements _i96.AnnouncementDetailsInteractor { + @override + _i8.Future<_i97.AnnouncementViewState?> getAnnouncement( + String? announcementId, + _i98.AnnouncementType? type, + String? courseId, + String? institutionToolbarTitle, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getAnnouncement, + [ + announcementId, + type, + courseId, + institutionToolbarTitle, + forceRefresh, + ], + ), + returnValue: _i8.Future<_i97.AnnouncementViewState?>.value(), + returnValueForMissingStub: + _i8.Future<_i97.AnnouncementViewState?>.value(), + ) as _i8.Future<_i97.AnnouncementViewState?>); + @override + void viewAttachment( + _i17.BuildContext? context, + _i99.Attachment? attachment, + ) => + super.noSuchMethod( + Invocation.method( + #viewAttachment, + [ + context, + attachment, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AnnouncementApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAnnouncementApi extends _i1.Mock implements _i100.AnnouncementApi { + @override + _i8.Future<_i101.Announcement?> getCourseAnnouncement( + String? courseId, + String? announcementId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getCourseAnnouncement, + [ + courseId, + announcementId, + forceRefresh, + ], + ), + returnValue: _i8.Future<_i101.Announcement?>.value(), + returnValueForMissingStub: _i8.Future<_i101.Announcement?>.value(), + ) as _i8.Future<_i101.Announcement?>); + @override + _i8.Future<_i102.AccountNotification?> getAccountNotification( + String? accountNotificationId, + bool? forceRefresh, + ) => + (super.noSuchMethod( + Invocation.method( + #getAccountNotification, + [ + accountNotificationId, + forceRefresh, + ], + ), + returnValue: _i8.Future<_i102.AccountNotification?>.value(), + returnValueForMissingStub: + _i8.Future<_i102.AccountNotification?>.value(), + ) as _i8.Future<_i102.AccountNotification?>); +} + +/// A class which mocks [AcceptableUsePolicyInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAcceptableUsePolicyInteractor extends _i1.Mock + implements _i103.AcceptableUsePolicyInteractor { + @override + _i8.Future<_i25.TermsOfService?> getTermsOfService() => (super.noSuchMethod( + Invocation.method( + #getTermsOfService, + [], + ), + returnValue: _i8.Future<_i25.TermsOfService?>.value(), + returnValueForMissingStub: _i8.Future<_i25.TermsOfService?>.value(), + ) as _i8.Future<_i25.TermsOfService?>); + @override + _i8.Future<_i61.User?> acceptTermsOfUse() => (super.noSuchMethod( + Invocation.method( + #acceptTermsOfUse, + [], + ), + returnValue: _i8.Future<_i61.User?>.value(), + returnValueForMissingStub: _i8.Future<_i61.User?>.value(), + ) as _i8.Future<_i61.User?>); +} + +/// A class which mocks [PlannerApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlannerApi extends _i1.Mock implements _i104.PlannerApi { + @override + _i8.Future?> getUserPlannerItems( + String? userId, + DateTime? startDay, + DateTime? endDay, { + Set? contexts = const {}, + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #getUserPlannerItems, + [ + userId, + startDay, + endDay, + ], + { + #contexts: contexts, + #forceRefresh: forceRefresh, + }, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); +} + +/// A class which mocks [HelpScreenInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHelpScreenInteractor extends _i1.Mock + implements _i106.HelpScreenInteractor { + @override + _i8.Future> getObserverCustomHelpLinks( + {bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getObserverCustomHelpLinks, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future>.value(<_i107.HelpLink>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i107.HelpLink>[]), + ) as _i8.Future>); + @override + bool containsObserverLinks(_i108.BuiltList<_i107.HelpLink>? links) => + (super.noSuchMethod( + Invocation.method( + #containsObserverLinks, + [links], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + List<_i107.HelpLink> filterObserverLinks( + _i108.BuiltList<_i107.HelpLink>? list) => + (super.noSuchMethod( + Invocation.method( + #filterObserverLinks, + [list], + ), + returnValue: <_i107.HelpLink>[], + returnValueForMissingStub: <_i107.HelpLink>[], + ) as List<_i107.HelpLink>); +} + +/// A class which mocks [FileApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileApi extends _i1.Mock implements _i109.FileApi { + @override + _i8.Future<_i99.Attachment?> uploadConversationFile( + _i10.File? file, + _i2.ProgressCallback? progressCallback, + ) => + (super.noSuchMethod( + Invocation.method( + #uploadConversationFile, + [ + file, + progressCallback, + ], + ), + returnValue: _i8.Future<_i99.Attachment?>.value(), + returnValueForMissingStub: _i8.Future<_i99.Attachment?>.value(), + ) as _i8.Future<_i99.Attachment?>); + @override + _i8.Future<_i10.File> downloadFile( + String? url, + String? savePath, { + _i2.CancelToken? cancelToken, + _i2.ProgressCallback? onProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #downloadFile, + [ + url, + savePath, + ], + { + #cancelToken: cancelToken, + #onProgress: onProgress, + }, + ), + returnValue: _i8.Future<_i10.File>.value(_FakeFile_28( + this, + Invocation.method( + #downloadFile, + [ + url, + savePath, + ], + { + #cancelToken: cancelToken, + #onProgress: onProgress, + }, + ), + )), + returnValueForMissingStub: _i8.Future<_i10.File>.value(_FakeFile_28( + this, + Invocation.method( + #downloadFile, + [ + url, + savePath, + ], + { + #cancelToken: cancelToken, + #onProgress: onProgress, + }, + ), + )), + ) as _i8.Future<_i10.File>); + @override + _i8.Future deleteFile(String? fileId) => (super.noSuchMethod( + Invocation.method( + #deleteFile, + [fileId], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [PathProviderVeneer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPathProviderVeneer extends _i1.Mock + implements _i110.PathProviderVeneer { + @override + _i8.Future<_i10.Directory> getTemporaryDirectory() => (super.noSuchMethod( + Invocation.method( + #getTemporaryDirectory, + [], + ), + returnValue: _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getTemporaryDirectory, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getTemporaryDirectory, + [], + ), + )), + ) as _i8.Future<_i10.Directory>); + @override + _i8.Future<_i10.Directory> getApplicationSupportDirectory() => + (super.noSuchMethod( + Invocation.method( + #getApplicationSupportDirectory, + [], + ), + returnValue: _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getApplicationSupportDirectory, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getApplicationSupportDirectory, + [], + ), + )), + ) as _i8.Future<_i10.Directory>); + @override + _i8.Future<_i10.Directory> getLibraryDirectory() => (super.noSuchMethod( + Invocation.method( + #getLibraryDirectory, + [], + ), + returnValue: _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getLibraryDirectory, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getLibraryDirectory, + [], + ), + )), + ) as _i8.Future<_i10.Directory>); + @override + _i8.Future<_i10.Directory> getApplicationDocumentsDirectory() => + (super.noSuchMethod( + Invocation.method( + #getApplicationDocumentsDirectory, + [], + ), + returnValue: _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getApplicationDocumentsDirectory, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i10.Directory>.value(_FakeDirectory_29( + this, + Invocation.method( + #getApplicationDocumentsDirectory, + [], + ), + )), + ) as _i8.Future<_i10.Directory>); + @override + _i8.Future<_i10.Directory?> getExternalStorageDirectory() => + (super.noSuchMethod( + Invocation.method( + #getExternalStorageDirectory, + [], + ), + returnValue: _i8.Future<_i10.Directory?>.value(), + returnValueForMissingStub: _i8.Future<_i10.Directory?>.value(), + ) as _i8.Future<_i10.Directory?>); + @override + _i8.Future?> getExternalCacheDirectories() => + (super.noSuchMethod( + Invocation.method( + #getExternalCacheDirectories, + [], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future?> getExternalStorageDirectories( + {_i111.StorageDirectory? type}) => + (super.noSuchMethod( + Invocation.method( + #getExternalStorageDirectories, + [], + {#type: type}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); +} + +/// A class which mocks [InboxCountNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInboxCountNotifier extends _i1.Mock + implements _i18.InboxCountNotifier { + @override + set value(int? newValue) => super.noSuchMethod( + Invocation.setter( + #value, + newValue, + ), + returnValueForMissingStub: null, + ); + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + void addListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i17.BuildContext { + @override + _i17.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_30( + this, + Invocation.getter(#widget), + ), + returnValueForMissingStub: _FakeWidget_30( + this, + Invocation.getter(#widget), + ), + ) as _i17.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i17.InheritedWidget dependOnInheritedElement( + _i17.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_31( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + returnValueForMissingStub: _FakeInheritedWidget_31( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i17.InheritedWidget); + @override + void visitAncestorElements(_i17.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i17.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i17.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i11.DiagnosticsNode describeElement( + String? name, { + _i11.DiagnosticsTreeStyle? style = _i11.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_32( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_32( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i11.DiagnosticsNode); + @override + _i11.DiagnosticsNode describeWidget( + String? name, { + _i11.DiagnosticsTreeStyle? style = _i11.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_32( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_32( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i11.DiagnosticsNode); + @override + List<_i11.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i11.DiagnosticsNode>[], + returnValueForMissingStub: <_i11.DiagnosticsNode>[], + ) as List<_i11.DiagnosticsNode>); + @override + _i11.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_32( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_32( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i11.DiagnosticsNode); +} + +/// A class which mocks [ConversationDetailsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConversationDetailsInteractor extends _i1.Mock + implements _i112.ConversationDetailsInteractor { + @override + _i8.Future<_i65.Conversation?> getConversation(String? id) => + (super.noSuchMethod( + Invocation.method( + #getConversation, + [id], + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); + @override + _i8.Future<_i65.Conversation?> addReply( + _i17.BuildContext? context, + _i65.Conversation? conversation, + _i113.Message? message, + bool? replyAll, + ) => + (super.noSuchMethod( + Invocation.method( + #addReply, + [ + context, + conversation, + message, + replyAll, + ], + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); + @override + _i8.Future viewAttachment( + _i17.BuildContext? context, + _i99.Attachment? attachment, + ) => + (super.noSuchMethod( + Invocation.method( + #viewAttachment, + [ + context, + attachment, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [ConversationListInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConversationListInteractor extends _i1.Mock + implements _i114.ConversationListInteractor { + @override + _i8.Future> getConversations( + {bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getConversations, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: + _i8.Future>.value(<_i65.Conversation>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i65.Conversation>[]), + ) as _i8.Future>); + @override + _i8.Future?> getCoursesForCompose() => (super.noSuchMethod( + Invocation.method( + #getCoursesForCompose, + [], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future?> getStudentEnrollments() => + (super.noSuchMethod( + Invocation.method( + #getStudentEnrollments, + [], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + List<_i115.Tuple2<_i61.User, _i51.Course>> combineEnrollmentsAndCourses( + List<_i51.Course>? courses, + List<_i58.Enrollment>? enrollments, + ) => + (super.noSuchMethod( + Invocation.method( + #combineEnrollmentsAndCourses, + [ + courses, + enrollments, + ], + ), + returnValue: <_i115.Tuple2<_i61.User, _i51.Course>>[], + returnValueForMissingStub: <_i115.Tuple2<_i61.User, _i51.Course>>[], + ) as List<_i115.Tuple2<_i61.User, _i51.Course>>); +} + +/// A class which mocks [ConversationReplyInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConversationReplyInteractor extends _i1.Mock + implements _i116.ConversationReplyInteractor { + @override + _i8.Future<_i65.Conversation?> createReply( + _i65.Conversation? conversation, + _i113.Message? message, + String? body, + List? attachmentIds, + bool? replyAll, + ) => + (super.noSuchMethod( + Invocation.method( + #createReply, + [ + conversation, + message, + body, + attachmentIds, + replyAll, + ], + ), + returnValue: _i8.Future<_i65.Conversation?>.value(), + returnValueForMissingStub: _i8.Future<_i65.Conversation?>.value(), + ) as _i8.Future<_i65.Conversation?>); + @override + _i8.Future<_i66.AttachmentHandler?> addAttachment( + _i17.BuildContext? context) => + (super.noSuchMethod( + Invocation.method( + #addAttachment, + [context], + ), + returnValue: _i8.Future<_i66.AttachmentHandler?>.value(), + returnValueForMissingStub: _i8.Future<_i66.AttachmentHandler?>.value(), + ) as _i8.Future<_i66.AttachmentHandler?>); + @override + String getCurrentUserId() => (super.noSuchMethod( + Invocation.method( + #getCurrentUserId, + [], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); +} + +/// A class which mocks [DomainSearchInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDomainSearchInteractor extends _i1.Mock + implements _i117.DomainSearchInteractor { + @override + _i8.Future?> performSearch(String? query) => + (super.noSuchMethod( + Invocation.method( + #performSearch, + [query], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); +} + +/// A class which mocks [DashboardInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDashboardInteractor extends _i1.Mock + implements _i118.DashboardInteractor { + @override + _i8.Future?> getStudents({bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getStudents, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + _i8.Future<_i61.User?> getSelf({dynamic app}) => (super.noSuchMethod( + Invocation.method( + #getSelf, + [], + {#app: app}, + ), + returnValue: _i8.Future<_i61.User?>.value(), + returnValueForMissingStub: _i8.Future<_i61.User?>.value(), + ) as _i8.Future<_i61.User?>); + @override + void sortUsers(List<_i61.User>? users) => super.noSuchMethod( + Invocation.method( + #sortUsers, + [users], + ), + returnValueForMissingStub: null, + ); + @override + _i18.InboxCountNotifier getInboxCountNotifier() => (super.noSuchMethod( + Invocation.method( + #getInboxCountNotifier, + [], + ), + returnValue: _FakeInboxCountNotifier_33( + this, + Invocation.method( + #getInboxCountNotifier, + [], + ), + ), + returnValueForMissingStub: _FakeInboxCountNotifier_33( + this, + Invocation.method( + #getInboxCountNotifier, + [], + ), + ), + ) as _i18.InboxCountNotifier); + @override + _i19.AlertCountNotifier getAlertCountNotifier() => (super.noSuchMethod( + Invocation.method( + #getAlertCountNotifier, + [], + ), + returnValue: _FakeAlertCountNotifier_34( + this, + Invocation.method( + #getAlertCountNotifier, + [], + ), + ), + returnValueForMissingStub: _FakeAlertCountNotifier_34( + this, + Invocation.method( + #getAlertCountNotifier, + [], + ), + ), + ) as _i19.AlertCountNotifier); + @override + _i8.Future shouldShowOldReminderMessage() => (super.noSuchMethod( + Invocation.method( + #shouldShowOldReminderMessage, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future<_i119.PermissionStatus> requestNotificationPermission() => + (super.noSuchMethod( + Invocation.method( + #requestNotificationPermission, + [], + ), + returnValue: _i8.Future<_i119.PermissionStatus>.value( + _i119.PermissionStatus.denied), + returnValueForMissingStub: _i8.Future<_i119.PermissionStatus>.value( + _i119.PermissionStatus.denied), + ) as _i8.Future<_i119.PermissionStatus>); +} + +/// A class which mocks [QRLoginTutorialScreenInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockQRLoginTutorialScreenInteractor extends _i1.Mock + implements _i20.QRLoginTutorialScreenInteractor { + @override + _i8.Future<_i20.BarcodeScanResult> scan() => (super.noSuchMethod( + Invocation.method( + #scan, + [], + ), + returnValue: + _i8.Future<_i20.BarcodeScanResult>.value(_FakeBarcodeScanResult_35( + this, + Invocation.method( + #scan, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i20.BarcodeScanResult>.value(_FakeBarcodeScanResult_35( + this, + Invocation.method( + #scan, + [], + ), + )), + ) as _i8.Future<_i20.BarcodeScanResult>); +} + +/// A class which mocks [ManageStudentsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockManageStudentsInteractor extends _i1.Mock + implements _i120.ManageStudentsInteractor { + @override + _i8.Future?> getStudents({bool? forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getStudents, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: _i8.Future?>.value(), + ) as _i8.Future?>); + @override + void sortUsers(List<_i61.User>? users) => super.noSuchMethod( + Invocation.method( + #sortUsers, + [users], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [StudentColorPickerInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockStudentColorPickerInteractor extends _i1.Mock + implements _i121.StudentColorPickerInteractor { + @override + _i8.Future save( + String? studentId, + _i35.Color? newColor, + ) => + (super.noSuchMethod( + Invocation.method( + #save, + [ + studentId, + newColor, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [UserApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUserApi extends _i1.Mock implements _i122.UserApi { + @override + _i8.Future<_i61.User?> getSelf() => (super.noSuchMethod( + Invocation.method( + #getSelf, + [], + ), + returnValue: _i8.Future<_i61.User?>.value(), + returnValueForMissingStub: _i8.Future<_i61.User?>.value(), + ) as _i8.Future<_i61.User?>); + @override + _i8.Future<_i61.User?> getUserForDomain( + String? domain, + String? userId, + ) => + (super.noSuchMethod( + Invocation.method( + #getUserForDomain, + [ + domain, + userId, + ], + ), + returnValue: _i8.Future<_i61.User?>.value(), + returnValueForMissingStub: _i8.Future<_i61.User?>.value(), + ) as _i8.Future<_i61.User?>); + @override + _i8.Future<_i61.UserPermission?> getSelfPermissions() => (super.noSuchMethod( + Invocation.method( + #getSelfPermissions, + [], + ), + returnValue: _i8.Future<_i61.UserPermission?>.value(), + returnValueForMissingStub: _i8.Future<_i61.UserPermission?>.value(), + ) as _i8.Future<_i61.UserPermission?>); + @override + _i8.Future<_i91.UserColors?> getUserColors({bool? refresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getUserColors, + [], + {#refresh: refresh}, + ), + returnValue: _i8.Future<_i91.UserColors?>.value(), + returnValueForMissingStub: _i8.Future<_i91.UserColors?>.value(), + ) as _i8.Future<_i91.UserColors?>); + @override + _i8.Future<_i61.User?> acceptUserTermsOfUse() => (super.noSuchMethod( + Invocation.method( + #acceptUserTermsOfUse, + [], + ), + returnValue: _i8.Future<_i61.User?>.value(), + returnValueForMissingStub: _i8.Future<_i61.User?>.value(), + ) as _i8.Future<_i61.User?>); + @override + _i8.Future<_i123.ColorChangeResponse?> setUserColor( + String? contextId, + _i35.Color? color, + ) => + (super.noSuchMethod( + Invocation.method( + #setUserColor, + [ + contextId, + color, + ], + ), + returnValue: _i8.Future<_i123.ColorChangeResponse?>.value(), + returnValueForMissingStub: + _i8.Future<_i123.ColorChangeResponse?>.value(), + ) as _i8.Future<_i123.ColorChangeResponse?>); +} + +/// A class which mocks [MasqueradeScreenInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMasqueradeScreenInteractor extends _i1.Mock + implements _i124.MasqueradeScreenInteractor { + @override + _i8.Future startMasquerading( + String? masqueradingUserId, + String? masqueradingDomain, + ) => + (super.noSuchMethod( + Invocation.method( + #startMasquerading, + [ + masqueradingUserId, + masqueradingDomain, + ], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + String sanitizeDomain(String? domain) => (super.noSuchMethod( + Invocation.method( + #sanitizeDomain, + [domain], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); +} + +/// A class which mocks [SettingsInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSettingsInteractor extends _i1.Mock + implements _i125.SettingsInteractor { + @override + bool isDebugMode() => (super.noSuchMethod( + Invocation.method( + #isDebugMode, + [], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + void routeToThemeViewer(_i17.BuildContext? context) => super.noSuchMethod( + Invocation.method( + #routeToThemeViewer, + [context], + ), + returnValueForMissingStub: null, + ); + @override + void routeToRemoteConfig(_i17.BuildContext? context) => super.noSuchMethod( + Invocation.method( + #routeToRemoteConfig, + [context], + ), + returnValueForMissingStub: null, + ); + @override + void toggleDarkMode( + _i17.BuildContext? context, + _i17.GlobalKey<_i17.State<_i17.StatefulWidget>>? anchorKey, + ) => + super.noSuchMethod( + Invocation.method( + #toggleDarkMode, + [ + context, + anchorKey, + ], + ), + returnValueForMissingStub: null, + ); + @override + void toggleHCMode(dynamic context) => super.noSuchMethod( + Invocation.method( + #toggleHCMode, + [context], + ), + returnValueForMissingStub: null, + ); + @override + void showAboutDialog(dynamic context) => super.noSuchMethod( + Invocation.method( + #showAboutDialog, + [context], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [FeaturesApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFeaturesApi extends _i1.Mock implements _i126.FeaturesApi { + @override + _i8.Future<_i127.FeatureFlags?> getFeatureFlags() => (super.noSuchMethod( + Invocation.method( + #getFeatureFlags, + [], + ), + returnValue: _i8.Future<_i127.FeatureFlags?>.value(), + returnValueForMissingStub: _i8.Future<_i127.FeatureFlags?>.value(), + ) as _i8.Future<_i127.FeatureFlags?>); +} + +/// A class which mocks [SplashScreenInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSplashScreenInteractor extends _i1.Mock + implements _i128.SplashScreenInteractor { + @override + _i8.Future<_i128.SplashScreenData?> getData({String? qrLoginUrl}) => + (super.noSuchMethod( + Invocation.method( + #getData, + [], + {#qrLoginUrl: qrLoginUrl}, + ), + returnValue: _i8.Future<_i128.SplashScreenData?>.value(), + returnValueForMissingStub: _i8.Future<_i128.SplashScreenData?>.value(), + ) as _i8.Future<_i128.SplashScreenData?>); + @override + _i8.Future updateUserColors() => (super.noSuchMethod( + Invocation.method( + #updateUserColors, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future getCameraCount() => (super.noSuchMethod( + Invocation.method( + #getCameraCount, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future isTermsAcceptanceRequired() => (super.noSuchMethod( + Invocation.method( + #isTermsAcceptanceRequired, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [AttachmentFetcherInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAttachmentFetcherInteractor extends _i1.Mock + implements _i129.AttachmentFetcherInteractor { + @override + _i8.Future<_i10.File> fetchAttachmentFile( + _i99.Attachment? attachment, + _i2.CancelToken? cancelToken, + ) => + (super.noSuchMethod( + Invocation.method( + #fetchAttachmentFile, + [ + attachment, + cancelToken, + ], + ), + returnValue: _i8.Future<_i10.File>.value(_FakeFile_28( + this, + Invocation.method( + #fetchAttachmentFile, + [ + attachment, + cancelToken, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i10.File>.value(_FakeFile_28( + this, + Invocation.method( + #fetchAttachmentFile, + [ + attachment, + cancelToken, + ], + ), + )), + ) as _i8.Future<_i10.File>); + @override + _i8.Future getAttachmentSavePath(_i99.Attachment? attachment) => + (super.noSuchMethod( + Invocation.method( + #getAttachmentSavePath, + [attachment], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i2.CancelToken generateCancelToken() => (super.noSuchMethod( + Invocation.method( + #generateCancelToken, + [], + ), + returnValue: _FakeCancelToken_36( + this, + Invocation.method( + #generateCancelToken, + [], + ), + ), + returnValueForMissingStub: _FakeCancelToken_36( + this, + Invocation.method( + #generateCancelToken, + [], + ), + ), + ) as _i2.CancelToken); +} + +/// A class which mocks [CancelToken]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCancelToken extends _i1.Mock implements _i2.CancelToken { + @override + set requestOptions(_i2.RequestOptions? _requestOptions) => super.noSuchMethod( + Invocation.setter( + #requestOptions, + _requestOptions, + ), + returnValueForMissingStub: null, + ); + @override + bool get isCancelled => (super.noSuchMethod( + Invocation.getter(#isCancelled), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i8.Future<_i2.DioError> get whenCancel => (super.noSuchMethod( + Invocation.getter(#whenCancel), + returnValue: _i8.Future<_i2.DioError>.value(_FakeDioError_37( + this, + Invocation.getter(#whenCancel), + )), + returnValueForMissingStub: + _i8.Future<_i2.DioError>.value(_FakeDioError_37( + this, + Invocation.getter(#whenCancel), + )), + ) as _i8.Future<_i2.DioError>); + @override + void cancel([dynamic reason]) => super.noSuchMethod( + Invocation.method( + #cancel, + [reason], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AudioVideoAttachmentViewerInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAudioVideoAttachmentViewerInteractor extends _i1.Mock + implements _i130.AudioVideoAttachmentViewerInteractor { + @override + _i21.VideoPlayerController? makeController(String? url) => + (super.noSuchMethod( + Invocation.method( + #makeController, + [url], + ), + returnValueForMissingStub: null, + ) as _i21.VideoPlayerController?); +} + +/// A class which mocks [VideoPlayerController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVideoPlayerController extends _i1.Mock + implements _i21.VideoPlayerController { + @override + String get dataSource => (super.noSuchMethod( + Invocation.getter(#dataSource), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + Map get httpHeaders => (super.noSuchMethod( + Invocation.getter(#httpHeaders), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + @override + _i21.DataSourceType get dataSourceType => (super.noSuchMethod( + Invocation.getter(#dataSourceType), + returnValue: _i21.DataSourceType.asset, + returnValueForMissingStub: _i21.DataSourceType.asset, + ) as _i21.DataSourceType); + @override + int get textureId => (super.noSuchMethod( + Invocation.getter(#textureId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + _i8.Future get position => (super.noSuchMethod( + Invocation.getter(#position), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i21.VideoPlayerValue get value => (super.noSuchMethod( + Invocation.getter(#value), + returnValue: _FakeVideoPlayerValue_38( + this, + Invocation.getter(#value), + ), + returnValueForMissingStub: _FakeVideoPlayerValue_38( + this, + Invocation.getter(#value), + ), + ) as _i21.VideoPlayerValue); + @override + set value(_i21.VideoPlayerValue? newValue) => super.noSuchMethod( + Invocation.setter( + #value, + newValue, + ), + returnValueForMissingStub: null, + ); + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i8.Future initialize() => (super.noSuchMethod( + Invocation.method( + #initialize, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future dispose() => (super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future play() => (super.noSuchMethod( + Invocation.method( + #play, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setLooping(bool? looping) => (super.noSuchMethod( + Invocation.method( + #setLooping, + [looping], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future seekTo(Duration? position) => (super.noSuchMethod( + Invocation.method( + #seekTo, + [position], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setVolume(double? volume) => (super.noSuchMethod( + Invocation.method( + #setVolume, + [volume], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setPlaybackSpeed(double? speed) => (super.noSuchMethod( + Invocation.method( + #setPlaybackSpeed, + [speed], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + void setCaptionOffset(Duration? offset) => super.noSuchMethod( + Invocation.method( + #setCaptionOffset, + [offset], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future setClosedCaptionFile( + _i8.Future<_i21.ClosedCaptionFile>? closedCaptionFile) => + (super.noSuchMethod( + Invocation.method( + #setClosedCaptionFile, + [closedCaptionFile], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + void removeListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void addListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PermissionHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPermissionHandler extends _i1.Mock + implements _i131.PermissionHandler { + @override + _i8.Future<_i119.PermissionStatus> checkPermissionStatus( + _i119.Permission? permission) => + (super.noSuchMethod( + Invocation.method( + #checkPermissionStatus, + [permission], + ), + returnValue: _i8.Future<_i119.PermissionStatus>.value( + _i119.PermissionStatus.denied), + returnValueForMissingStub: _i8.Future<_i119.PermissionStatus>.value( + _i119.PermissionStatus.denied), + ) as _i8.Future<_i119.PermissionStatus>); + @override + _i8.Future<_i119.PermissionStatus> requestPermission( + _i119.Permission? permission) => + (super.noSuchMethod( + Invocation.method( + #requestPermission, + [permission], + ), + returnValue: _i8.Future<_i119.PermissionStatus>.value( + _i119.PermissionStatus.denied), + returnValueForMissingStub: _i8.Future<_i119.PermissionStatus>.value( + _i119.PermissionStatus.denied), + ) as _i8.Future<_i119.PermissionStatus>); +} + +/// A class which mocks [FlutterDownloaderVeneer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterDownloaderVeneer extends _i1.Mock + implements _i132.FlutterDownloaderVeneer { + @override + _i8.Future enqueue({ + required String? url, + required String? savedDir, + String? fileName, + bool? showNotification = true, + bool? openFileFromNotification = true, + bool? requiresStorageNotLow = true, + bool? saveInPublicStorage = true, + }) => + (super.noSuchMethod( + Invocation.method( + #enqueue, + [], + { + #url: url, + #savedDir: savedDir, + #fileName: fileName, + #showNotification: showNotification, + #openFileFromNotification: openFileFromNotification, + #requiresStorageNotLow: requiresStorageNotLow, + #saveInPublicStorage: saveInPublicStorage, + }, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future?> loadTasks() => (super.noSuchMethod( + Invocation.method( + #loadTasks, + [], + ), + returnValue: _i8.Future?>.value(), + returnValueForMissingStub: + _i8.Future?>.value(), + ) as _i8.Future?>); +} + +/// A class which mocks [ViewAttachmentInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockViewAttachmentInteractor extends _i1.Mock + implements _i134.ViewAttachmentInteractor { + @override + _i8.Future openExternally(_i99.Attachment? attachment) => + (super.noSuchMethod( + Invocation.method( + #openExternally, + [attachment], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future downloadFile(_i99.Attachment? attachment) => + (super.noSuchMethod( + Invocation.method( + #downloadFile, + [attachment], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [AuthenticationInterceptor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthenticationInterceptor extends _i1.Mock + implements _i135.AuthenticationInterceptor { + @override + _i8.Future onError( + _i2.DioError? error, + _i2.ErrorInterceptorHandler? handler, + ) => + (super.noSuchMethod( + Invocation.method( + #onError, + [ + error, + handler, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + void onRequest( + _i2.RequestOptions? options, + _i2.RequestInterceptorHandler? handler, + ) => + super.noSuchMethod( + Invocation.method( + #onRequest, + [ + options, + handler, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onResponse( + _i2.Response? response, + _i2.ResponseInterceptorHandler? handler, + ) => + super.noSuchMethod( + Invocation.method( + #onResponse, + [ + response, + handler, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [ErrorInterceptorHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockErrorInterceptorHandler extends _i1.Mock + implements _i2.ErrorInterceptorHandler { + @override + _i8.Future<_i22.InterceptorState> get future => (super.noSuchMethod( + Invocation.getter(#future), + returnValue: _i8.Future<_i22.InterceptorState>.value( + _FakeInterceptorState_39( + this, + Invocation.getter(#future), + )), + returnValueForMissingStub: + _i8.Future<_i22.InterceptorState>.value( + _FakeInterceptorState_39( + this, + Invocation.getter(#future), + )), + ) as _i8.Future<_i22.InterceptorState>); + @override + bool get isCompleted => (super.noSuchMethod( + Invocation.getter(#isCompleted), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + void next(_i2.DioError? err) => super.noSuchMethod( + Invocation.method( + #next, + [err], + ), + returnValueForMissingStub: null, + ); + @override + void resolve(_i2.Response? response) => super.noSuchMethod( + Invocation.method( + #resolve, + [response], + ), + returnValueForMissingStub: null, + ); + @override + void reject(_i2.DioError? error) => super.noSuchMethod( + Invocation.method( + #reject, + [error], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AttachmentPickerInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAttachmentPickerInteractor extends _i1.Mock + implements _i136.AttachmentPickerInteractor { + @override + _i8.Future<_i10.File?> getImageFromCamera() => (super.noSuchMethod( + Invocation.method( + #getImageFromCamera, + [], + ), + returnValue: _i8.Future<_i10.File?>.value(), + returnValueForMissingStub: _i8.Future<_i10.File?>.value(), + ) as _i8.Future<_i10.File?>); + @override + _i8.Future<_i10.File?> getFileFromDevice() => (super.noSuchMethod( + Invocation.method( + #getFileFromDevice, + [], + ), + returnValue: _i8.Future<_i10.File?>.value(), + returnValueForMissingStub: _i8.Future<_i10.File?>.value(), + ) as _i8.Future<_i10.File?>); + @override + _i8.Future<_i10.File?> getImageFromGallery() => (super.noSuchMethod( + Invocation.method( + #getImageFromGallery, + [], + ), + returnValue: _i8.Future<_i10.File?>.value(), + returnValueForMissingStub: _i8.Future<_i10.File?>.value(), + ) as _i8.Future<_i10.File?>); +} + +/// A class which mocks [AttachmentHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAttachmentHandler extends _i1.Mock implements _i66.AttachmentHandler { + @override + set onStageChange( + dynamic Function(_i66.AttachmentUploadStage)? _onStageChange) => + super.noSuchMethod( + Invocation.setter( + #onStageChange, + _onStageChange, + ), + returnValueForMissingStub: null, + ); + @override + set progress(double? _progress) => super.noSuchMethod( + Invocation.setter( + #progress, + _progress, + ), + returnValueForMissingStub: null, + ); + @override + set attachment(_i99.Attachment? _attachment) => super.noSuchMethod( + Invocation.setter( + #attachment, + _attachment, + ), + returnValueForMissingStub: null, + ); + @override + _i66.AttachmentUploadStage get stage => (super.noSuchMethod( + Invocation.getter(#stage), + returnValue: _i66.AttachmentUploadStage.CREATED, + returnValueForMissingStub: _i66.AttachmentUploadStage.CREATED, + ) as _i66.AttachmentUploadStage); + @override + set stage(_i66.AttachmentUploadStage? stage) => super.noSuchMethod( + Invocation.setter( + #stage, + stage, + ), + returnValueForMissingStub: null, + ); + @override + String get displayName => (super.noSuchMethod( + Invocation.getter(#displayName), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i8.Future performUpload() => (super.noSuchMethod( + Invocation.method( + #performUpload, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future cleanUpFile() => (super.noSuchMethod( + Invocation.method( + #cleanUpFile, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future deleteAttachment() => (super.noSuchMethod( + Invocation.method( + #deleteAttachment, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + void addListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeListener(_i35.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [OldAppMigration]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockOldAppMigration extends _i1.Mock implements _i137.OldAppMigration { + @override + _i8.Future performMigrationIfNecessary() => (super.noSuchMethod( + Invocation.method( + #performMigrationIfNecessary, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future hasOldReminders() => (super.noSuchMethod( + Invocation.method( + #hasOldReminders, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); +} + +/// A class which mocks [HelpLinksApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHelpLinksApi extends _i1.Mock implements _i138.HelpLinksApi { + @override + _i8.Future<_i139.HelpLinks?> getHelpLinks({dynamic forceRefresh = false}) => + (super.noSuchMethod( + Invocation.method( + #getHelpLinks, + [], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i8.Future<_i139.HelpLinks?>.value(), + returnValueForMissingStub: _i8.Future<_i139.HelpLinks?>.value(), + ) as _i8.Future<_i139.HelpLinks?>); +} + +/// A class which mocks [RemoteConfigInteractor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRemoteConfigInteractor extends _i1.Mock + implements _i140.RemoteConfigInteractor { + @override + Map<_i141.RemoteConfigParams, String> getRemoteConfigParams() => + (super.noSuchMethod( + Invocation.method( + #getRemoteConfigParams, + [], + ), + returnValue: <_i141.RemoteConfigParams, String>{}, + returnValueForMissingStub: <_i141.RemoteConfigParams, String>{}, + ) as Map<_i141.RemoteConfigParams, String>); + @override + void updateRemoteConfig( + _i141.RemoteConfigParams? rcKey, + String? value, + ) => + super.noSuchMethod( + Invocation.method( + #updateRemoteConfig, + [ + rcKey, + value, + ], + ), + returnValueForMissingStub: null, + ); +} diff --git a/apps/flutter_parent/test/utils/test_utils.dart b/apps/flutter_parent/test/utils/test_utils.dart index d110eb97b8..77de22075d 100644 --- a/apps/flutter_parent/test/utils/test_utils.dart +++ b/apps/flutter_parent/test/utils/test_utils.dart @@ -54,19 +54,15 @@ double _getScreenHeightOffset(WidgetTester tester, ScreenVerticalLocation locati Future ensureVisibleByScrolling( Finder finder, WidgetTester widgetTester, { - ScreenVerticalLocation scrollFrom, + required ScreenVerticalLocation scrollFrom, Offset scrollBy = const Offset(0, -50), int maxScrolls = 100, }) async { - assert(finder != null); - assert(scrollFrom != null); - assert(scrollBy != null); - assert(maxScrolls != null); final scrollFromY = _getScreenHeightOffset(widgetTester, scrollFrom); final gesture = await widgetTester.startGesture(Offset(0, scrollFromY)); - Widget foundWidget; + Widget? foundWidget; for (var i = 0; i < maxScrolls; ++i) { await gesture.moveBy(scrollBy); diff --git a/apps/flutter_parent/test/utils/veneer/android_intent_veneer_test.dart b/apps/flutter_parent/test/utils/veneer/android_intent_veneer_test.dart index 70217d793c..a1dd626d1b 100644 --- a/apps/flutter_parent/test/utils/veneer/android_intent_veneer_test.dart +++ b/apps/flutter_parent/test/utils/veneer/android_intent_veneer_test.dart @@ -32,7 +32,7 @@ void main() { final emailBody = 'multi\r\nline\r\nbody\r\n'; final completer = Completer(); - await MethodChannel('intent').setMockMethodCallHandler((MethodCall call) async { + MethodChannel('intent').setMockMethodCallHandler((MethodCall call) async { expect(call.method, 'startActivity'); expect(call.arguments['action'], 'android.intent.action.SENDTO'); expect(call.arguments['data'], 'mailto:'); @@ -55,7 +55,7 @@ void main() { var telUri = 'tel:+123'; final completer = Completer(); - await MethodChannel('intent').setMockMethodCallHandler((MethodCall call) async { + MethodChannel('intent').setMockMethodCallHandler((MethodCall call) async { expect(call.method, 'startActivity'); expect(call.arguments['action'], 'android.intent.action.DIAL'); expect(call.arguments['data'], Uri.parse(telUri).toString()); @@ -73,7 +73,7 @@ void main() { var mailto = 'mailto:pandas@instructure.com'; final completer = Completer(); - await MethodChannel('intent').setMockMethodCallHandler((MethodCall call) async { + MethodChannel('intent').setMockMethodCallHandler((MethodCall call) async { expect(call.method, 'startActivity'); expect(call.arguments['action'], 'android.intent.action.SENDTO'); expect(call.arguments['data'], Uri.parse(mailto).toString()); diff --git a/apps/flutter_parent/test/utils/widgets/avatar_test.dart b/apps/flutter_parent/test/utils/widgets/avatar_test.dart index 83c716d3be..568c4fcc85 100644 --- a/apps/flutter_parent/test/utils/widgets/avatar_test.dart +++ b/apps/flutter_parent/test/utils/widgets/avatar_test.dart @@ -24,7 +24,7 @@ void main() { test('Question mark when short name is empty or null', () { var blank = ''; - var nullName = null; + String? nullName = null; expect(Avatar.getUserInitials(blank), equals('?')); expect(Avatar.getUserInitials(nullName), equals('?')); diff --git a/apps/flutter_parent/test/utils/widgets/badges_test.dart b/apps/flutter_parent/test/utils/widgets/badges_test.dart index 7893afce8f..42378a6a35 100644 --- a/apps/flutter_parent/test/utils/widgets/badges_test.dart +++ b/apps/flutter_parent/test/utils/widgets/badges_test.dart @@ -33,7 +33,7 @@ void main() { expect(decoration.color, StudentColorSet.all[0].light); var state = tester.state(find.byType(MaterialApp)); - ParentTheme.of(state.context).setSelectedStudent('1'); + ParentTheme.of(state.context)?.setSelectedStudent('1'); await tester.pumpAndSettle(); decoration = (tester.widgetList(find.byType(Container)).last as Container).decoration as BoxDecoration; @@ -117,7 +117,7 @@ void main() { expect((border.decoration as BoxDecoration).color, Colors.white); expect((background.decoration as BoxDecoration).color, StudentColorSet.electric.light); - expect(text.style.color, Colors.white); + expect(text.style?.color, Colors.white); }); testWidgetsWithAccessibilityChecks('has a white border with a high contrast blue background', (tester) async { @@ -133,7 +133,7 @@ void main() { expect((border.decoration as BoxDecoration).color, Colors.white); expect((background.decoration as BoxDecoration).color, StudentColorSet.electric.lightHC); - expect(text.style.color, Colors.white); + expect(text.style?.color, Colors.white); }); testWidgetsWithAccessibilityChecks('has a black border with a blue background in dark mode', (tester) async { @@ -149,7 +149,7 @@ void main() { expect((border.decoration as BoxDecoration).color, Colors.black); expect((background.decoration as BoxDecoration).color, StudentColorSet.electric.dark); - expect(text.style.color, Colors.black); + expect(text.style?.color, Colors.black); }); testWidgetsWithAccessibilityChecks('has a black border with a high contrast blue background in dark mode', @@ -167,7 +167,7 @@ void main() { expect((border.decoration as BoxDecoration).color, Colors.black); expect((background.decoration as BoxDecoration).color, StudentColorSet.electric.darkHC); - expect(text.style.color, Colors.black); + expect(text.style?.color, Colors.black); }); // HAMBURGER TESTS @@ -183,7 +183,7 @@ void main() { expect((border.decoration as BoxDecoration).color, StudentColorSet.electric.light); expect((background.decoration as BoxDecoration).color, Colors.white); - expect(text.style.color, StudentColorSet.electric.light); + expect(text.style?.color, StudentColorSet.electric.light); }); testWidgetsWithAccessibilityChecks('hamburger has a high contrast blue border with a white background', @@ -200,7 +200,7 @@ void main() { expect((border.decoration as BoxDecoration).color, StudentColorSet.electric.lightHC); expect((background.decoration as BoxDecoration).color, Colors.white); - expect(text.style.color, StudentColorSet.electric.lightHC); + expect(text.style?.color, StudentColorSet.electric.lightHC); }); testWidgetsWithAccessibilityChecks('hamburger has a black border with a tiara background in dark mode', @@ -217,7 +217,7 @@ void main() { expect((border.decoration as BoxDecoration).color, Colors.black); expect((background.decoration as BoxDecoration).color, ParentColors.tiara); - expect(text.style.color, Colors.black); + expect(text.style?.color, Colors.black); }); testWidgetsWithAccessibilityChecks('hamburger has a black border with a tiara background in dark mode and HC', @@ -234,15 +234,11 @@ void main() { expect((border.decoration as BoxDecoration).color, Colors.black); expect((background.decoration as BoxDecoration).color, ParentColors.tiara); - expect(text.style.color, Colors.black); + expect(text.style?.color, Colors.black); }); }); group('WidgetBadge', () { - test('throws if null is passed in for the child icon', () { - expect(() => WidgetBadge(null), throwsAssertionError); - }); - testWidgetsWithAccessibilityChecks('shows the widget passed in', (tester) async { final child = Icon(Icons.error); await tester.pumpWidget(TestApp(WidgetBadge(child))); diff --git a/apps/flutter_parent/test/utils/widgets/empty_panda_widget_test.dart b/apps/flutter_parent/test/utils/widgets/empty_panda_widget_test.dart index bc5be0c5c9..d54b7d171b 100644 --- a/apps/flutter_parent/test/utils/widgets/empty_panda_widget_test.dart +++ b/apps/flutter_parent/test/utils/widgets/empty_panda_widget_test.dart @@ -56,7 +56,7 @@ void main() { await tester.pumpWidget(_testableWidget(EmptyPandaWidget(buttonText: buttonText))); await tester.pumpAndSettle(); - expect(find.widgetWithText(FlatButton, buttonText), findsOneWidget); + expect(find.widgetWithText(TextButton, buttonText), findsOneWidget); }); testWidgetsWithAccessibilityChecks('tapping button invokes callback', (tester) async { @@ -66,7 +66,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.widgetWithText(FlatButton, buttonText), findsOneWidget); + expect(find.widgetWithText(TextButton, buttonText), findsOneWidget); await tester.tap(find.text(buttonText)); expect(called, isTrue); }); @@ -130,7 +130,7 @@ void main() { expect(find.byType(Text), findsNWidgets(3)); expect(find.text(title), findsOneWidget); expect(find.text(subtitle), findsOneWidget); - expect(find.widgetWithText(FlatButton, buttonText), findsOneWidget); + expect(find.widgetWithText(TextButton, buttonText), findsOneWidget); }, skip: true); testWidgetsWithAccessibilityChecks('shows a header', (tester) async { diff --git a/apps/flutter_parent/test/utils/widgets/error_panda_widget_test.dart b/apps/flutter_parent/test/utils/widgets/error_panda_widget_test.dart index 3329ac6a7e..d6baa6d739 100644 --- a/apps/flutter_parent/test/utils/widgets/error_panda_widget_test.dart +++ b/apps/flutter_parent/test/utils/widgets/error_panda_widget_test.dart @@ -22,9 +22,9 @@ import '../test_app.dart'; void main() { var errorString = AppLocalizations().errorLoadingMessages; - var callback = null; + Function? callback = null; - testWidgetsWithAccessibilityChecks('Shows warning icon', (tester) async { + testWidgetsWithAccessibilityChecks('Shows warning icon', (WidgetTester tester) async { await tester.pumpWidget(TestApp( ErrorPandaWidget(errorString, callback), )); @@ -48,7 +48,7 @@ void main() { )); await tester.pumpAndSettle(); - expect(find.byType(FlatButton), findsOneWidget); + expect(find.byType(TextButton), findsOneWidget); expect(find.text(AppLocalizations().retry), findsOneWidget); }); @@ -63,7 +63,7 @@ void main() { await tester.pumpAndSettle(); // Click retry button - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // Verify the callback was called @@ -76,7 +76,7 @@ void main() { )); await tester.pumpAndSettle(); - expect(find.byType(FlatButton), findsOneWidget); + expect(find.byType(TextButton), findsOneWidget); expect(find.text(AppLocalizations().retry), findsOneWidget); expect(find.text('header here'), findsOneWidget); }); diff --git a/apps/flutter_parent/test/utils/widgets/error_report/error_report_dialog_test.dart b/apps/flutter_parent/test/utils/widgets/error_report/error_report_dialog_test.dart index 0df807e7e0..30b108947b 100644 --- a/apps/flutter_parent/test/utils/widgets/error_report/error_report_dialog_test.dart +++ b/apps/flutter_parent/test/utils/widgets/error_report/error_report_dialog_test.dart @@ -22,6 +22,7 @@ import 'package:mockito/mockito.dart'; import '../../accessibility_utils.dart'; import '../../test_app.dart'; import '../../test_helpers/mock_helpers.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { testWidgetsWithAccessibilityChecks('Shows a dialog', (tester) async { diff --git a/apps/flutter_parent/test/utils/widgets/error_report/error_report_interactor_test.dart b/apps/flutter_parent/test/utils/widgets/error_report/error_report_interactor_test.dart index 016967517b..38737a52a8 100644 --- a/apps/flutter_parent/test/utils/widgets/error_report/error_report_interactor_test.dart +++ b/apps/flutter_parent/test/utils/widgets/error_report/error_report_interactor_test.dart @@ -25,6 +25,7 @@ import '../../canvas_model_utils.dart'; import '../../platform_config.dart'; import '../../test_app.dart'; import '../../test_helpers/mock_helpers.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { final user = User((b) => b @@ -136,7 +137,7 @@ void main() { await ErrorReportInteractor().submitErrorReport('', '', '', ErrorReportSeverity.COMMENT, ''); verify(api.submitErrorReport( - domain: ErrorReportApi.DEFAULT_DOMAIN, + domain: anyNamed('domain'), subject: anyNamed('subject'), description: anyNamed('description'), email: anyNamed('email'), diff --git a/apps/flutter_parent/test/utils/widgets/full_screen_scroll_container_test.dart b/apps/flutter_parent/test/utils/widgets/full_screen_scroll_container_test.dart index 37e78d14f8..731d229536 100644 --- a/apps/flutter_parent/test/utils/widgets/full_screen_scroll_container_test.dart +++ b/apps/flutter_parent/test/utils/widgets/full_screen_scroll_container_test.dart @@ -20,10 +20,6 @@ import 'package:mockito/mockito.dart'; import '../accessibility_utils.dart'; void main() { - test('throws error when horizontal padding is null', () { - expect(() => FullScreenScrollContainer(children: [], horizontalPadding: null), throwsAssertionError); - }); - group('Single child', () { testWidgetsWithAccessibilityChecks('is visible', (tester) async { final children = [Text('a')]; @@ -170,7 +166,7 @@ class _Refresher extends Mock { void refresh(); } -Widget _refreshingWidget(List children, {_Refresher refresher, Widget header}) { +Widget _refreshingWidget(List children, {_Refresher? refresher, Widget? header}) { return MaterialApp( home: Scaffold( body: RefreshIndicator( diff --git a/apps/flutter_parent/test/utils/widgets/masquerade_ui_test.dart b/apps/flutter_parent/test/utils/widgets/masquerade_ui_test.dart index 7be74bd948..2c06d6883c 100644 --- a/apps/flutter_parent/test/utils/widgets/masquerade_ui_test.dart +++ b/apps/flutter_parent/test/utils/widgets/masquerade_ui_test.dart @@ -32,6 +32,7 @@ import '../canvas_model_utils.dart'; import '../platform_config.dart'; import '../test_app.dart'; import '../test_helpers/mock_helpers.dart'; +import '../test_helpers/mock_helpers.mocks.dart'; void main() { AppLocalizations l10n = AppLocalizations(); @@ -45,7 +46,7 @@ void main() { ..masqueradeDomain = 'masqueradeDomain' ..masqueradeUser = CanvasModelTestUtils.mockUser(name: 'Masked User').toBuilder()); - String masqueradeText = l10n.actingAsUser(masqueradeLogin.masqueradeUser.name); + String masqueradeText = l10n.actingAsUser(masqueradeLogin.masqueradeUser!.name); Key masqueradeContainerKey = Key('masquerade-ui-container'); @@ -94,7 +95,7 @@ void main() { // Foreground border Container container = tester.widget(find.byKey(masqueradeContainerKey)); - Border border = (container.foregroundDecoration as BoxDecoration).border; + Border border = (container.foregroundDecoration as BoxDecoration).border as Border; expect(border.left, BorderSide(color: ParentColors.masquerade, width: 3)); expect(border.top, BorderSide(color: ParentColors.masquerade, width: 3)); expect(border.right, BorderSide(color: ParentColors.masquerade, width: 3)); @@ -116,14 +117,14 @@ void main() { expect(find.text(masqueradeText), findsOneWidget); ApiPrefs.switchLogins(normalLogin); - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // Should now be disabled expect(find.text(masqueradeText), findsNothing); ApiPrefs.switchLogins(masqueradeLogin); - await tester.tap(find.byType(FlatButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // Should be enabled again @@ -139,7 +140,7 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); - expect(find.text(l10n.endMasqueradeMessage(masqueradeLogin.masqueradeUser.name)), findsOneWidget); + expect(find.text(l10n.endMasqueradeMessage(masqueradeLogin.masqueradeUser!.name)), findsOneWidget); // Close the dialog await tester.tap(find.text(l10n.cancel)); @@ -159,7 +160,7 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); - expect(find.text(l10n.endMasqueradeLogoutMessage(masqueradeLogin.masqueradeUser.name)), findsOneWidget); + expect(find.text(l10n.endMasqueradeLogoutMessage(masqueradeLogin.masqueradeUser!.name)), findsOneWidget); }); testWidgetsWithAccessibilityChecks('Accepting confirmation dialog stops masquerading', (tester) async { @@ -184,8 +185,8 @@ void main() { testWidgetsWithAccessibilityChecks('Accepting logout confirmation performs logout', (tester) async { final reminderDb = MockReminderDb(); - final calendarFilterDb = _MockCalendarFilterDb(); - final notificationUtil = _MockNotificationUtil(); + final calendarFilterDb = MockCalendarFilterDb(); + final notificationUtil = MockNotificationUtil(); when(reminderDb.getAllForUser(any, any)).thenAnswer((_) async => []); setupTestLocator((locator) { locator.registerLazySingleton(() => reminderDb); @@ -213,17 +214,13 @@ void main() { }); } -class _MockNotificationUtil extends Mock implements NotificationUtil {} - -class _MockCalendarFilterDb extends Mock implements CalendarFilterDb {} - Widget _childWithButton() { return Material( child: Builder( - builder: (context) => FlatButton( + builder: (context) => TextButton( child: Text('Tap to refresh'), onPressed: () { - MasqueradeUI.of(context).refresh(); + MasqueradeUI.of(context)?.refresh(); }, ), ), diff --git a/apps/flutter_parent/test/utils/widgets/rating_dialog_test.dart b/apps/flutter_parent/test/utils/widgets/rating_dialog_test.dart index ffbd4a23b3..8e2b3c3e52 100644 --- a/apps/flutter_parent/test/utils/widgets/rating_dialog_test.dart +++ b/apps/flutter_parent/test/utils/widgets/rating_dialog_test.dart @@ -24,6 +24,7 @@ import 'package:mockito/mockito.dart'; import '../accessibility_utils.dart'; import '../test_app.dart'; import '../test_helpers/mock_helpers.dart'; +import '../test_helpers/mock_helpers.mocks.dart'; void main() { final analytics = MockAnalytics(); @@ -42,7 +43,7 @@ void main() { reset(launcher); }); - Future _showDialog(tester, {DateTime nextShowDate, bool dontShowAgain}) async { + Future _showDialog(tester, {DateTime? nextShowDate, bool? dontShowAgain}) async { return TestApp.showWidgetFromTap(tester, (context) => RatingDialog.showDialogIfPossible(context, false), configBlock: () async { await ApiPrefs.setRatingNextShowDate(nextShowDate); @@ -108,8 +109,8 @@ void main() { verify(analytics.logEvent(AnalyticsEventConstants.RATING_DIALOG_SHOW)).called(1); // Set four weeks from "now" ("now" is a little later when set in api prefs by rating dialog) - expect(ApiPrefs.getRatingNextShowDate().isAfter(date.add(Duration(days: RatingDialog.FOUR_WEEKS))), isTrue); - expect(ApiPrefs.getRatingNextShowDate().isBefore(date.add(Duration(days: RatingDialog.SIX_WEEKS))), isTrue); + expect(ApiPrefs.getRatingNextShowDate()?.isAfter(date.add(Duration(days: RatingDialog.FOUR_WEEKS))), isTrue); + expect(ApiPrefs.getRatingNextShowDate()?.isBefore(date.add(Duration(days: RatingDialog.SIX_WEEKS))), isTrue); expect(find.byType(RatingDialog), findsOneWidget); expect(find.text(AppLocalizations().ratingDialogTitle), findsOneWidget); expect(find.text(AppLocalizations().ratingDialogDontShowAgain.toUpperCase()), findsOneWidget); @@ -166,8 +167,8 @@ void main() { expect(find.byType(RatingDialog), findsNothing); expect(ApiPrefs.getRatingDontShowAgain(), isNot(true)); // Shouldn't set this if we are sending feedback // Four weeks when without a comment ("now" is a little later when set in api prefs by rating dialog) - expect(ApiPrefs.getRatingNextShowDate().isAfter(date.add(Duration(days: RatingDialog.FOUR_WEEKS))), isTrue); - expect(ApiPrefs.getRatingNextShowDate().isBefore(date.add(Duration(days: RatingDialog.SIX_WEEKS))), isTrue); + expect(ApiPrefs.getRatingNextShowDate()?.isAfter(date.add(Duration(days: RatingDialog.FOUR_WEEKS))), isTrue); + expect(ApiPrefs.getRatingNextShowDate()?.isBefore(date.add(Duration(days: RatingDialog.SIX_WEEKS))), isTrue); verify(analytics.logEvent( AnalyticsEventConstants.RATING_DIALOG, extras: {AnalyticsParamConstants.STAR_RATING: 1}, // First was clicked, should send a 1 @@ -268,7 +269,7 @@ void main() { expect(find.byType(RatingDialog), findsNothing); expect(ApiPrefs.getRatingDontShowAgain(), isNull); // Shouldn't set this if we are sending feedback // Six weeks when given a comment - expect(ApiPrefs.getRatingNextShowDate().isAfter(date.add(Duration(days: RatingDialog.SIX_WEEKS))), isTrue); + expect(ApiPrefs.getRatingNextShowDate()?.isAfter(date.add(Duration(days: RatingDialog.SIX_WEEKS))), isTrue); verify(intentVeneer.launchEmailWithBody(AppLocalizations().ratingDialogEmailSubject('1.0.0'), emailBody)); verify(analytics.logEvent( AnalyticsEventConstants.RATING_DIALOG, diff --git a/apps/flutter_parent/test/utils/widgets/respawn_test.dart b/apps/flutter_parent/test/utils/widgets/respawn_test.dart index 31a93deefc..9b22d55373 100644 --- a/apps/flutter_parent/test/utils/widgets/respawn_test.dart +++ b/apps/flutter_parent/test/utils/widgets/respawn_test.dart @@ -70,16 +70,16 @@ class _RespawnTestWidgetState extends State<_RespawnTestWidget> { 'Count: $_counter', key: _RespawnTestWidget.counterKey, ), - FlatButton( + TextButton( key: _RespawnTestWidget.incrementKey, child: Text('Tap to increment'), onPressed: () => setState(() => _counter++), ), - FlatButton( + TextButton( key: _RespawnTestWidget.respawnKey, child: Text('Tap to Respawn'), onPressed: () { - Respawn.of(context).restart(); + Respawn.of(context)?.restart(); }, ), ], diff --git a/apps/flutter_parent/test/utils/widgets/user_name_test.dart b/apps/flutter_parent/test/utils/widgets/user_name_test.dart index 28f2bf1471..d35747e983 100644 --- a/apps/flutter_parent/test/utils/widgets/user_name_test.dart +++ b/apps/flutter_parent/test/utils/widgets/user_name_test.dart @@ -18,7 +18,7 @@ import 'package:test/test.dart'; void main() { test('text returns only username if pronouns is null', () { String name = 'User Name'; - String pronouns = null; + String? pronouns = null; UserName userName = UserName(name, pronouns); expect(userName.text, name); diff --git a/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_interactor_test.dart b/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_interactor_test.dart index aaf5eaa593..cc65ebfc71 100644 --- a/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_interactor_test.dart +++ b/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_interactor_test.dart @@ -25,6 +25,7 @@ import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; import '../../../test_app.dart'; +import '../../../test_helpers/mock_helpers.mocks.dart'; void main() { setUp(() { @@ -72,7 +73,7 @@ void main() { test('fetchAttachmentFile calls fileApi with correct parameters', () async { var attachment = _makeAttachment(); - var fileApi = _MockFileApi(); + var fileApi = MockFileApi(); _setupLocator((locator) { locator.registerLazySingleton(() => fileApi); @@ -84,7 +85,7 @@ void main() { verify( fileApi.downloadFile( - attachment.url, + attachment.url!, 'cache/attachment-123-fake-file.txt', cancelToken: cancelToken, ), @@ -99,7 +100,7 @@ void main() { await cachedFile.writeAsString('This is a test'); var attachment = _makeAttachment().rebuild((b) => b..size = 14); - var fileApi = _MockFileApi(); + var fileApi = MockFileApi(); _setupLocator((locator) { locator.registerLazySingleton(() => fileApi); @@ -122,7 +123,7 @@ void main() { await cachedFile.writeAsString('This is a test but the file size does not match'); var attachment = _makeAttachment().rebuild((b) => b..size = 14); - var fileApi = _MockFileApi(); + var fileApi = MockFileApi(); _setupLocator((locator) { locator.registerLazySingleton(() => fileApi); @@ -142,8 +143,8 @@ void main() { }); } -_setupLocator([config(GetIt locator) = null]) async { - var pathProvider = _MockPathProvider(); +_setupLocator([config(GetIt locator)? = null]) async { + var pathProvider = MockPathProviderVeneer(); await setupTestLocator((locator) { locator.registerLazySingleton(() => pathProvider); if (config != null) config(locator); @@ -158,8 +159,4 @@ Attachment _makeAttachment() { ..filename = 'fake-file.txt' ..size = 14 // File size for text file with the contents 'This is a test' ..url = 'https://fake.url.com/fake-file.txt'); -} - -class _MockPathProvider extends Mock implements PathProviderVeneer {} - -class _MockFileApi extends Mock implements FileApi {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_test.dart b/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_test.dart index d6608a0d8c..e10bf00bd8 100644 --- a/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_test.dart +++ b/apps/flutter_parent/test/utils/widgets/view_attachments/fetcher/attachment_fetcher_test.dart @@ -29,16 +29,18 @@ import 'package:mockito/mockito.dart'; import '../../../accessibility_utils.dart'; import '../../../test_app.dart'; +import '../../../test_helpers/mock_helpers.mocks.dart'; void main() { testWidgetsWithAccessibilityChecks('displays loading state', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); Completer completer = Completer(); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => completer.future); + when(interactor.generateCancelToken()).thenAnswer((_) => CancelToken()); await tester.pumpWidget( TestApp(Material(child: AttachmentFetcher(attachment: Attachment(), builder: (_, __) => Container()))), @@ -49,15 +51,16 @@ void main() { }); testWidgetsWithAccessibilityChecks('provides correct file to builder', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); var expectedFile = File('fakefile.exe'); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => Future.value(expectedFile)); + when(interactor.generateCancelToken()).thenAnswer((_) => CancelToken()); - File actualFile; + late File actualFile; await tester.pumpWidget( TestApp(Material( child: AttachmentFetcher( @@ -74,13 +77,14 @@ void main() { }); testWidgetsWithAccessibilityChecks('builds child on success', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); var expectedFile = File('fakefile.exe'); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => Future.value(expectedFile)); + when(interactor.generateCancelToken()).thenAnswer((_) => CancelToken()); await tester.pumpWidget( TestApp(Material( @@ -95,12 +99,13 @@ void main() { }); testWidgetsWithAccessibilityChecks('displays error state', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => Future.error('')); + when(interactor.generateCancelToken()).thenAnswer((_) => CancelToken()); await tester.pumpWidget( TestApp( @@ -114,12 +119,13 @@ void main() { }); testWidgetsWithAccessibilityChecks('performs retry', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => Future.error('')); + when(interactor.generateCancelToken()).thenAnswer((_) => CancelToken()); await tester.pumpWidget( TestApp( @@ -140,28 +146,31 @@ void main() { }); testWidgetsWithAccessibilityChecks('cancels request on dispose', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => Future.error('')); - var cancelToken = _MockCancelToken(); + var cancelToken = MockCancelToken(); when(interactor.generateCancelToken()).thenReturn(cancelToken); await tester.pumpWidget( TestApp( Builder( builder: (context) => Center( - child: RaisedButton( - color: Colors.white, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + ), onPressed: () => QuickNav().push( context, Material(child: AttachmentFetcher(attachment: Attachment(), builder: (_, __) => Container())), ), child: Text( 'click me', + style: Theme.of(context).textTheme.bodyMedium, ), ), ), @@ -170,18 +179,14 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.byType(RaisedButton)); + await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(AttachmentFetcher), findsOneWidget); - TestApp.navigatorKey.currentState.pop(); + TestApp.navigatorKey.currentState?.pop(); await tester.pumpAndSettle(); verify(cancelToken.cancel()).called(1); }); } - -class _MockInteractor extends Mock implements AttachmentFetcherInteractor {} - -class _MockCancelToken extends Mock implements CancelToken {} diff --git a/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_interactor_test.dart b/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_interactor_test.dart index 48709859fb..eefa47bc5a 100644 --- a/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_interactor_test.dart +++ b/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_interactor_test.dart @@ -27,117 +27,13 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:test/test.dart'; import '../../test_app.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { - test('openExternally calls AndroidIntent with correct parameters', () async { - var intentVeneer = _MockAndroidIntentVeneer(); - await setupTestLocator((locator) { - locator.registerLazySingleton(() => intentVeneer); - }); - - Attachment attachment = Attachment((a) => a - ..url = 'fake_url' - ..contentType = 'fake/type'); - - await ViewAttachmentInteractor().openExternally(attachment); - - AndroidIntent intent = verify(intentVeneer.launch(captureAny)).captured[0]; - expect(intent.action, 'action_view'); - expect(intent.data, attachment.url); - expect(intent.type, attachment.inferContentType()); - }); - - test('checkStoragePermission returns true when permission is already granted', () async { - var permissionHandler = _MockPermissionHandler(); - await setupTestLocator((locator) { - locator.registerLazySingleton(() => permissionHandler); - }); - - when(permissionHandler.checkPermissionStatus(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.granted)); - - var isGranted = await ViewAttachmentInteractor().checkStoragePermission(); - // requestPermissions should not have been called - verifyNever(permissionHandler.requestPermission(any)); - - // Should return true - expect(isGranted, isTrue); - }); - - test('checkStoragePermission returns false when permission is rejected', () async { - var permissionHandler = _MockPermissionHandler(); - await setupTestLocator((locator) { - locator.registerLazySingleton(() => permissionHandler); - }); - - when(permissionHandler.checkPermissionStatus(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.denied)); - - when(permissionHandler.requestPermission(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.denied)); - - var isGranted = await ViewAttachmentInteractor().checkStoragePermission(); - - // requestPermissions should have been called once - verify(permissionHandler.requestPermission(Permission.storage)).called(1); - - // Should return true - expect(isGranted, isFalse); - }); - - test('checkStoragePermission returns true when permission is request and granted', () async { - var permissionHandler = _MockPermissionHandler(); - await setupTestLocator((locator) { - locator.registerLazySingleton(() => permissionHandler); - }); - - when(permissionHandler.checkPermissionStatus(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.denied)); - - when(permissionHandler.requestPermission(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.granted)); - - var isGranted = await ViewAttachmentInteractor().checkStoragePermission(); - - // requestPermissions should have been called once - verify(permissionHandler.requestPermission(Permission.storage)).called(1); - - // Should return true - expect(isGranted, isTrue); - }); - - test('downloadFile does nothing when permission is not granted', () async { - var permissionHandler = _MockPermissionHandler(); - var pathProvider = _MockPathProvider(); - var downloader = _MockDownloader(); - await setupTestLocator((locator) { - locator.registerLazySingleton(() => permissionHandler); - locator.registerLazySingleton(() => pathProvider); - locator.registerLazySingleton(() => downloader); - }); - - when(permissionHandler.checkPermissionStatus(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.denied)); - when(permissionHandler.requestPermission(Permission.storage)) - .thenAnswer((_) => Future.value(PermissionStatus.denied)); - - ViewAttachmentInteractor().downloadFile(Attachment()); - - verifyNever(pathProvider.getExternalStorageDirectories(type: anyNamed('type'))); - verifyNever( - downloader.enqueue( - url: anyNamed('url'), - savedDir: anyNamed('savedDir'), - showNotification: anyNamed('showNotification'), - openFileFromNotification: anyNamed('openFileFromNotification'), - ), - ); - }); - test('downloadFile calls PathProvider and FlutterDownloader with correct parameters', () async { - var permissionHandler = _MockPermissionHandler(); - var pathProvider = _MockPathProvider(); - var downloader = _MockDownloader(); + var permissionHandler = MockPermissionHandler(); + var pathProvider = MockPathProviderVeneer(); + var downloader = MockFlutterDownloaderVeneer(); await setupTestLocator((locator) { locator.registerLazySingleton(() => permissionHandler); locator.registerLazySingleton(() => pathProvider); @@ -166,11 +62,3 @@ void main() { ); }); } - -class _MockPathProvider extends Mock implements PathProviderVeneer {} - -class _MockPermissionHandler extends Mock implements PermissionHandler {} - -class _MockDownloader extends Mock implements FlutterDownloaderVeneer {} - -class _MockAndroidIntentVeneer extends Mock implements AndroidIntentVeneer {} diff --git a/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_screen_test.dart b/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_screen_test.dart index cb7b5e12ba..f3a08baf4f 100644 --- a/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_screen_test.dart +++ b/apps/flutter_parent/test/utils/widgets/view_attachments/view_attachment_screen_test.dart @@ -29,6 +29,7 @@ import 'package:mockito/mockito.dart'; import '../../accessibility_utils.dart'; import '../../network_image_response.dart'; import '../../test_app.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { mockNetworkImageResponse(); @@ -36,17 +37,17 @@ void main() { AppLocalizations l10n = AppLocalizations(); testWidgetsWithAccessibilityChecks('shows attachment display name', (tester) async { - setupTestLocator((locator) => locator.registerFactory(() => _MockInteractor())); + setupTestLocator((locator) => locator.registerFactory(() => MockViewAttachmentInteractor())); Attachment attachment = Attachment((a) => a..displayName = 'Display Name'); await tester.pumpWidget(TestApp(ViewAttachmentScreen(attachment))); await tester.pumpAndSettle(); - expect(find.descendant(of: find.byType(AppBar), matching: find.text(attachment.displayName)), findsOneWidget); + expect(find.descendant(of: find.byType(AppBar), matching: find.text(attachment.displayName!)), findsOneWidget); }); testWidgetsWithAccessibilityChecks('shows overflow menu', (tester) async { - setupTestLocator((locator) => locator.registerFactory(() => _MockInteractor())); + setupTestLocator((locator) => locator.registerFactory(() => MockViewAttachmentInteractor())); Attachment attachment = Attachment((a) => a..displayName = 'Display Name'); await tester.pumpWidget(TestApp(ViewAttachmentScreen(attachment))); @@ -63,7 +64,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('download button calls interactor', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockViewAttachmentInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); Attachment attachment = Attachment((a) => a..displayName = 'Display Name'); @@ -79,7 +80,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('open externally button calls interactor', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockViewAttachmentInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); Attachment attachment = Attachment((a) => a..displayName = 'Display Name'); @@ -97,7 +98,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('displays snackbar when open externally fails', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockViewAttachmentInteractor(); setupTestLocator((locator) => locator.registerFactory(() => interactor)); Attachment attachment = Attachment((a) => a..displayName = 'Display Name'); @@ -119,7 +120,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('shows attachment file name if display name is null', (tester) async { - setupTestLocator((locator) => locator.registerFactory(() => _MockInteractor())); + setupTestLocator((locator) => locator.registerFactory(() => MockViewAttachmentInteractor())); Attachment attachment = Attachment((a) => a ..displayName = null ..filename = 'File Name'); @@ -127,11 +128,11 @@ void main() { await tester.pumpWidget(TestApp(ViewAttachmentScreen(attachment))); await tester.pumpAndSettle(); - expect(find.descendant(of: find.byType(AppBar), matching: find.text(attachment.filename)), findsOneWidget); + expect(find.descendant(of: find.byType(AppBar), matching: find.text(attachment.filename!)), findsOneWidget); }); testWidgetsWithAccessibilityChecks('shows correct widget for images', (tester) async { - setupTestLocator((locator) => locator.registerFactory(() => _MockInteractor())); + setupTestLocator((locator) => locator.registerFactory(() => MockViewAttachmentInteractor())); Attachment attachment = Attachment((a) => a ..displayName = 'Display Name' ..url = 'fake_url' @@ -145,7 +146,7 @@ void main() { // TODO Fix test testWidgetsWithAccessibilityChecks('shows correct widget for videos', (tester) async { setupTestLocator((locator) { - locator.registerFactory(() => _MockInteractor()); + locator.registerFactory(() => MockViewAttachmentInteractor()); locator.registerFactory(() => AudioVideoAttachmentViewerInteractor()); }); Attachment attachment = Attachment((a) => a @@ -161,7 +162,7 @@ void main() { // TODO Fix test testWidgetsWithAccessibilityChecks('shows correct widget for audio', (tester) async { setupTestLocator((locator) { - locator.registerFactory(() => _MockInteractor()); + locator.registerFactory(() => MockViewAttachmentInteractor()); locator.registerFactory(() => AudioVideoAttachmentViewerInteractor()); }); Attachment attachment = Attachment((a) => a @@ -176,7 +177,7 @@ void main() { testWidgetsWithAccessibilityChecks('shows correct widget for text', (tester) async { setupTestLocator((locator) { - locator.registerFactory(() => _MockInteractor()); + locator.registerFactory(() => MockViewAttachmentInteractor()); locator.registerFactory(() => AttachmentFetcherInteractor()); }); Attachment attachment = Attachment((a) => a @@ -190,7 +191,7 @@ void main() { }); testWidgetsWithAccessibilityChecks('shows correct widget for unknown type', (tester) async { - setupTestLocator((locator) => locator.registerFactory(() => _MockInteractor())); + setupTestLocator((locator) => locator.registerFactory(() => MockViewAttachmentInteractor())); Attachment attachment = Attachment((a) => a ..displayName = 'Display Name' ..url = 'fake_url' @@ -200,6 +201,4 @@ void main() { expect(find.byType(UnknownAttachmentTypeViewer), findsOneWidget); }); -} - -class _MockInteractor extends Mock implements ViewAttachmentInteractor {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/audio_video_attachment_viewer_test.dart b/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/audio_video_attachment_viewer_test.dart index 0a9cb1bf7d..d76928b525 100644 --- a/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/audio_video_attachment_viewer_test.dart +++ b/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/audio_video_attachment_viewer_test.dart @@ -25,18 +25,20 @@ import 'package:flutter_parent/utils/design/canvas_icons.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:video_player/video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import '../../../accessibility_utils.dart'; import '../../../test_app.dart'; +import '../../../test_helpers/mock_helpers.mocks.dart'; void main() { testWidgetsWithAccessibilityChecks('displays loading indicator', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAudioVideoAttachmentViewerInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); - var controller = _MockVideoController(); + var controller = MockVideoPlayerController(); when(interactor.makeController(any)).thenReturn(controller); Completer initCompleter = Completer(); @@ -54,7 +56,7 @@ void main() { // TODO Fix test testWidgetsWithAccessibilityChecks('displays error widget', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAudioVideoAttachmentViewerInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -73,7 +75,7 @@ void main() { }, skip: true); testWidgetsWithAccessibilityChecks('displays error widget when controller is null', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAudioVideoAttachmentViewerInteractor(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -88,11 +90,12 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(EmptyPandaWidget), findsOneWidget); - }); + }, skip: true); // Testing w/o a11y checks due to minor issues in Chewie that we can't control testWidgets('displays video player', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAudioVideoAttachmentViewerInteractor(); + VideoPlayerPlatform.instance = _FakeVideoPlayerPlatform(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -112,7 +115,8 @@ void main() { // Testing w/o a11y checks due to minor issues in Chewie that we can't control testWidgets('displays audio icon for audio attachment', (tester) async { - var interactor = _MockInteractor(); + var interactor = MockAudioVideoAttachmentViewerInteractor(); + VideoPlayerPlatform.instance = _FakeVideoPlayerPlatform(); setupTestLocator((locator) { locator.registerFactory(() => interactor); }); @@ -131,19 +135,18 @@ void main() { }); } -class _MockInteractor extends Mock implements AudioVideoAttachmentViewerInteractor {} - -class _MockVideoController extends Mock implements VideoPlayerController {} - class _FakeVideoController extends Fake implements VideoPlayerController { final bool hasError; _FakeVideoController({this.hasError = false}); + + @override VideoPlayerValue get value => VideoPlayerValue( - duration: hasError ? null : Duration(seconds: 3), + duration: Duration(seconds: 3), errorDescription: hasError ? 'Error' : null, + isInitialized: true, ); @override @@ -156,7 +159,7 @@ class _FakeVideoController extends Fake implements VideoPlayerController { Future dispose() async => null; @override - Future play() => null; + Future play() async => null; @override int get textureId => 0; @@ -167,3 +170,98 @@ class _FakeVideoController extends Fake implements VideoPlayerController { @override void removeListener(listener) {} } + +class _FakeVideoPlayerPlatform extends VideoPlayerPlatform { + final Completer initialized = Completer(); + final List calls = []; + final List dataSources = []; + final Map> streams = + >{}; + final bool forceInitError; + int nextTextureId = 0; + final Map _positions = {}; + + _FakeVideoPlayerPlatform({ + this.forceInitError = false, + }); + + @override + Future create(DataSource dataSource) async { + calls.add('create'); + final StreamController stream = StreamController(); + streams[nextTextureId] = stream; + stream.add( + VideoEvent( + eventType: VideoEventType.initialized, + size: const Size(100, 100), + duration: const Duration(seconds: 1), + ), + ); + dataSources.add(dataSource); + return nextTextureId++; + } + + @override + Future dispose(int textureId) async { + calls.add('dispose'); + } + + @override + Future init() async { + calls.add('init'); + initialized.complete(true); + } + + @override + Stream videoEventsFor(int textureId) { + return streams[textureId]!.stream; + } + + @override + Future pause(int textureId) async { + calls.add('pause'); + } + + @override + Future play(int textureId) async { + calls.add('play'); + } + + @override + Future getPosition(int textureId) async { + calls.add('position'); + return _positions[textureId] ?? Duration.zero; + } + + @override + Future seekTo(int textureId, Duration position) async { + calls.add('seekTo'); + _positions[textureId] = position; + } + + @override + Future setLooping(int textureId, bool looping) async { + calls.add('setLooping'); + } + + @override + Future setVolume(int textureId, double volume) async { + calls.add('setVolume'); + } + + @override + Future setPlaybackSpeed(int textureId, double speed) async { + calls.add('setPlaybackSpeed'); + } + + @override + Future setMixWithOthers(bool mixWithOthers) async { + calls.add('setMixWithOthers'); + } + + @override + Widget buildView(int textureId) { + return Texture(textureId: textureId); + } +} + diff --git a/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/text_attachment_viewer_test.dart b/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/text_attachment_viewer_test.dart index 5f4a8930c5..0dffe7348a 100644 --- a/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/text_attachment_viewer_test.dart +++ b/apps/flutter_parent/test/utils/widgets/view_attachments/viewers/text_attachment_viewer_test.dart @@ -24,6 +24,7 @@ import 'package:mockito/mockito.dart'; import '../../../accessibility_utils.dart'; import '../../../test_app.dart'; +import '../../../test_helpers/mock_helpers.mocks.dart'; void main() { setUpAll(() async { @@ -55,7 +56,7 @@ void main() { await tester.pumpAndSettle(); var widget = await tester.widget(find.text(text)); - expect(widget.style.fontSize, 14); + expect(widget.style?.fontSize, 14); // Start two gestures separated by 100px var finger1 = await tester.startGesture(Offset(100, 300)); @@ -71,7 +72,7 @@ void main() { // Font size should have increased 200% (from 14 to 28) widget = await tester.widget(find.text(text)); - expect(widget.style.fontSize, 28); + expect(widget.style?.fontSize, 28); }); testWidgetsWithAccessibilityChecks('zooms to min font size of 10', (tester) async { @@ -82,7 +83,7 @@ void main() { await tester.pumpAndSettle(); var widget = await tester.widget(find.text(text)); - expect(widget.style.fontSize, 14); + expect(widget.style?.fontSize, 14); // Start two gestures separated by 400px var finger1 = await tester.startGesture(Offset(100, 300)); @@ -98,7 +99,7 @@ void main() { // Font size should have decreased to only 10 widget = await tester.widget(find.text(text)); - expect(widget.style.fontSize, 10); + expect(widget.style?.fontSize, 10); }); testWidgetsWithAccessibilityChecks('zooms to max font size of 48', (tester) async { @@ -109,7 +110,7 @@ void main() { await tester.pumpAndSettle(); var widget = await tester.widget(find.text(text)); - expect(widget.style.fontSize, 14); + expect(widget.style?.fontSize, 14); // Start two gestures separated by 100px var finger1 = await tester.startGesture(Offset(100, 300)); @@ -125,7 +126,7 @@ void main() { // Font size should have increased only 48 widget = await tester.widget(find.text(text)); - expect(widget.style.fontSize, 48); + expect(widget.style?.fontSize, 48); }); } @@ -135,12 +136,10 @@ _setupLocator(String text) { file.writeAsStringSync(text); // Set up interactor - var interactor = _MockInteractor(); + var interactor = MockAttachmentFetcherInteractor(); setupTestLocator((locator) { locator.registerLazySingleton(() => interactor); }); when(interactor.generateCancelToken()).thenReturn(CancelToken()); when(interactor.fetchAttachmentFile(any, any)).thenAnswer((_) => Future.value(file)); -} - -class _MockInteractor extends Mock implements AttachmentFetcherInteractor {} +} \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/widgets/webview/canvas_web_view_test.dart b/apps/flutter_parent/test/utils/widgets/webview/canvas_web_view_test.dart index 2c59f4ec66..c53d0c6d58 100644 --- a/apps/flutter_parent/test/utils/widgets/webview/canvas_web_view_test.dart +++ b/apps/flutter_parent/test/utils/widgets/webview/canvas_web_view_test.dart @@ -23,6 +23,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../../accessibility_utils.dart'; import '../../platform_config.dart'; import '../../test_app.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { final interactor = _MockWebViewInteractor(); @@ -33,24 +34,6 @@ void main() { reset(interactor); }); - group('constructor', () { - test('throws assertion error when initialHeight is null', () { - expect(() => CanvasWebView(content: null, initialHeight: null), throwsAssertionError); - }); - - test('throws assertion error when horizontalPadding is null', () { - expect(() => CanvasWebView(content: null, horizontalPadding: null), throwsAssertionError); - }); - - test('throws assertion error when authContentIfNecessary is null', () { - expect(() => CanvasWebView(content: null, authContentIfNecessary: null), throwsAssertionError); - }); - - test('throws assertion error when fullScreen is null', () { - expect(() => CanvasWebView(content: null, fullScreen: null), throwsAssertionError); - }); - }); - group('completely empty', () { testWidgetsWithAccessibilityChecks('Shows an empty container with null content and null label', (tester) async { await tester.pumpWidget(TestApp(CanvasWebView(content: null))); @@ -155,9 +138,9 @@ void main() { }); } -class _MockWebViewInteractor extends Mock implements WebContentInteractor { +class _MockWebViewInteractor extends Mock implements MockWebContentInteractor { @override JavascriptChannel ltiToolPressedChannel(handler) { - return WebContentInteractor().ltiToolPressedChannel(handler); + return WebContentInteractor().ltiToolPressedChannel(handler!); } } diff --git a/apps/flutter_parent/test/utils/widgets/webview/html_description_screen_test.dart b/apps/flutter_parent/test/utils/widgets/webview/html_description_screen_test.dart index ba31504f33..024788048c 100644 --- a/apps/flutter_parent/test/utils/widgets/webview/html_description_screen_test.dart +++ b/apps/flutter_parent/test/utils/widgets/webview/html_description_screen_test.dart @@ -19,6 +19,7 @@ import 'package:flutter_test/flutter_test.dart'; import '../../accessibility_utils.dart'; import '../../test_app.dart'; import '../../test_helpers/mock_helpers.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { setupTestLocator((locator) => locator..registerLazySingleton(() => MockWebContentInteractor())); diff --git a/apps/flutter_parent/test/utils/widgets/webview/html_description_tile_test.dart b/apps/flutter_parent/test/utils/widgets/webview/html_description_tile_test.dart index 5c0b81ddba..352f520342 100644 --- a/apps/flutter_parent/test/utils/widgets/webview/html_description_tile_test.dart +++ b/apps/flutter_parent/test/utils/widgets/webview/html_description_tile_test.dart @@ -23,6 +23,7 @@ import 'package:mockito/mockito.dart'; import '../../accessibility_utils.dart'; import '../../test_app.dart'; import '../../test_helpers/mock_helpers.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { final l10n = AppLocalizations(); diff --git a/apps/flutter_parent/test/utils/widgets/webview/web_content_interactor_test.dart b/apps/flutter_parent/test/utils/widgets/webview/web_content_interactor_test.dart index b67dd11e40..577b988278 100644 --- a/apps/flutter_parent/test/utils/widgets/webview/web_content_interactor_test.dart +++ b/apps/flutter_parent/test/utils/widgets/webview/web_content_interactor_test.dart @@ -24,6 +24,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../../platform_config.dart'; import '../../test_app.dart'; import '../../test_helpers/mock_helpers.dart'; +import '../../test_helpers/mock_helpers.mocks.dart'; void main() { final oauthApi = MockOAuthApi(); @@ -44,7 +45,7 @@ void main() { test('failure returns target_url', () async { final target = '$domain/target_url'; when(oauthApi.getAuthenticatedUrl(target)) - .thenAnswer((_) async => Future.error('Failed to authenticate url').catchError((_) {})); + .thenAnswer((_) async => Future.error('Failed to authenticate url').catchError((_) { return Future.value(null);})); final actual = await WebContentInteractor().getAuthUrl(target); expect(actual, target); @@ -155,7 +156,7 @@ void main() { }); } -String _makeIframe({String id, String src, String target, String ltiButtonText}) { +String _makeIframe({String? id, String? src, String? target, String? ltiButtonText}) { String ltiButton = ltiButtonText != null ? '

$ltiButtonText

' : ''; diff --git a/apps/flutter_parent/test_driver/apis/announcement_seed_api.dart b/apps/flutter_parent/test_driver/apis/announcement_seed_api.dart index 3622006b25..bef66eddfe 100644 --- a/apps/flutter_parent/test_driver/apis/announcement_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/announcement_seed_api.dart @@ -17,13 +17,15 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class AnnouncementSeedApi { - static Future createAnnouncement(String courseId, String title, String message) async { + static Future createAnnouncement(String courseId, String title, String message) async { var queryParams = { 'title': title, 'message': message, 'is_announcement': true, }; - return fetch(seedingDio().post('courses/$courseId/discussion_topics', queryParameters: queryParams)); + var dio = seedingDio(); + + return fetch(dio.post('courses/$courseId/discussion_topics', queryParameters: queryParams)); } } diff --git a/apps/flutter_parent/test_driver/apis/assignment_seed_api.dart b/apps/flutter_parent/test_driver/apis/assignment_seed_api.dart index 427ac66c85..30e53d27dc 100644 --- a/apps/flutter_parent/test_driver/apis/assignment_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/assignment_seed_api.dart @@ -23,8 +23,8 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class AssignmentSeedApi { - static Future createAssignment(String courseId, - {double pointsPossible = 20, DateTime dueAt = null, bool published = true}) async { + static Future createAssignment(String courseId, + {double pointsPossible = 20, DateTime? dueAt = null, bool published = true}) async { if (dueAt == null) dueAt = DateTime.now().add(Duration(days: 1)).toUtc(); final dish = faker.food.dish(); final assignmentName = dish + ' ' + faker.randomGenerator.integer(100, min: 1).toString(); diff --git a/apps/flutter_parent/test_driver/apis/calendar_seed_api.dart b/apps/flutter_parent/test_driver/apis/calendar_seed_api.dart index a5dd7f9c4a..f9ca276119 100644 --- a/apps/flutter_parent/test_driver/apis/calendar_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/calendar_seed_api.dart @@ -17,9 +17,9 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class CalendarSeedApi { - static Future createCalendarEvent(String courseId, String title, DateTime startAt, + static Future createCalendarEvent(String courseId, String title, DateTime startAt, {String description = "", - DateTime endAt = null, + DateTime? endAt = null, bool allDay = false, String locationName = "", String locationAddress = ""}) async { @@ -35,6 +35,8 @@ class CalendarSeedApi { 'calendar_event[location_address]': locationAddress, }; - return fetch(seedingDio().post('calendar_events', queryParameters: queryParams)); + var dio = seedingDio(); + + return fetch(dio.post('calendar_events', queryParameters: queryParams)); } } diff --git a/apps/flutter_parent/test_driver/apis/course_seed_api.dart b/apps/flutter_parent/test_driver/apis/course_seed_api.dart index 6d6f42b5d7..57e9d38e3e 100644 --- a/apps/flutter_parent/test_driver/apis/course_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/course_seed_api.dart @@ -22,7 +22,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class CourseSeedApi { - static Future createCourse({bool forceRefresh: false}) async { + static Future createCourse({bool forceRefresh = false}) async { final dio = seedingDio(); final courseNumber = faker.randomGenerator.integer(500, min: 100).toString(); final courseName = faker.sport.name() + " " + courseNumber; diff --git a/apps/flutter_parent/test_driver/apis/enrollment_seed_api.dart b/apps/flutter_parent/test_driver/apis/enrollment_seed_api.dart index a0f74215c3..2ad28ac6a7 100644 --- a/apps/flutter_parent/test_driver/apis/enrollment_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/enrollment_seed_api.dart @@ -21,7 +21,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class EnrollmentSeedApi { - static Future createEnrollment( + static Future createEnrollment( String userId, String courseId, String role, String associatedUserId) async { final dio = seedingDio(); final enrollmentWrapper = CreateEnrollmentWrapper((b) => b diff --git a/apps/flutter_parent/test_driver/apis/quiz_seed_api.dart b/apps/flutter_parent/test_driver/apis/quiz_seed_api.dart index bc9f1faaaf..321c2d5caa 100644 --- a/apps/flutter_parent/test_driver/apis/quiz_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/quiz_seed_api.dart @@ -17,7 +17,7 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class QuizSeedApi { - static Future createQuiz(String courseId, String title, DateTime dueAt, {String description = ""}) async { + static Future createQuiz(String courseId, String title, DateTime dueAt, {String description = ""}) async { var queryParams = { 'quiz[title]': title, 'quiz[description]': description, @@ -25,6 +25,7 @@ class QuizSeedApi { 'quiz[due_at]': dueAt.toIso8601String(), }; - return fetch(seedingDio().post('courses/$courseId/quizzes', queryParameters: queryParams)); + var dio = seedingDio(); + return fetch(dio.post('courses/$courseId/quizzes', queryParameters: queryParams)); } } diff --git a/apps/flutter_parent/test_driver/apis/submission_seed_api.dart b/apps/flutter_parent/test_driver/apis/submission_seed_api.dart index 88fc5c2f73..b3a438d87f 100644 --- a/apps/flutter_parent/test_driver/apis/submission_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/submission_seed_api.dart @@ -23,8 +23,8 @@ import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/network/utils/fetch.dart'; class SubmissionSeedApi { - static Future createSubmission(String courseId, Assignment assignment, String asUserId) { - SubmissionTypes submissionType = assignment.submissionTypes.first; + static Future createSubmission(String courseId, Assignment? assignment, String asUserId) async { + SubmissionTypes? submissionType = assignment?.submissionTypes?.first; String submissionTypeString = ""; switch (submissionType) { case SubmissionTypes.onlineTextEntry: @@ -37,8 +37,8 @@ class SubmissionSeedApi { "unknown"; break; } - String url = (submissionType == SubmissionTypes.onlineUrl) ? faker.internet.httpsUrl() : null; - String textBody = (submissionType == SubmissionTypes.onlineTextEntry) ? faker.lorem.sentence() : null; + String? url = (submissionType == SubmissionTypes.onlineUrl) ? faker.internet.httpsUrl() : null; + String? textBody = (submissionType == SubmissionTypes.onlineTextEntry) ? faker.lorem.sentence() : null; final submissionWrapper = CreateSubmissionWrapper((b) => b ..submission.body = textBody ..submission.url = url @@ -49,16 +49,16 @@ class SubmissionSeedApi { final dio = seedingDio(); print("submission postBody = $postBody"); - return fetch(dio.post("courses/$courseId/assignments/${assignment.id}/submissions", data: postBody)); + return fetch(dio.post("courses/$courseId/assignments/${assignment?.id}/submissions", data: postBody)); } - static Future gradeSubmission(String courseId, Assignment assignment, String studentId, String grade) { + static Future gradeSubmission(String courseId, Assignment? assignment, String studentId, String grade) async { final gradeWrapper = GradeSubmissionWrapper((b) => b..submission.postedGrade = grade); final postBody = json.encode(serialize(gradeWrapper)); final dio = seedingDio(); print("Grade submission postBody: $postBody"); - return fetch(dio.put("courses/$courseId/assignments/${assignment.id}/submissions/$studentId", data: postBody)); + return fetch(dio.put("courses/$courseId/assignments/${assignment?.id}/submissions/$studentId", data: postBody)); } } diff --git a/apps/flutter_parent/test_driver/apis/user_seed_api.dart b/apps/flutter_parent/test_driver/apis/user_seed_api.dart index 35a69af5c8..3dbdb3a126 100644 --- a/apps/flutter_parent/test_driver/apis/user_seed_api.dart +++ b/apps/flutter_parent/test_driver/apis/user_seed_api.dart @@ -37,7 +37,7 @@ const _createUserEndpoint = "accounts/self/users"; class UserSeedApi { static const authCodeChannel = const MethodChannel("GET_AUTH_CODE"); - static Future createUser() async { + static Future createUser() async { var url = baseSeedingUrl + _createUserEndpoint; var lastName = faker.person.lastName(); @@ -63,14 +63,14 @@ class UserSeedApi { if (response.statusCode == 200) { //print("Create User response: ${response.data}"); var result = deserialize(response.data); - result = result.rebuild((b) => b + result = result!.rebuild((b) => b ..loginId = userData.pseudonym.uniqueId ..password = userData.pseudonym.password ..domain = response.requestOptions.uri.host); - var verifyResult = await AuthApi().mobileVerify(result.domain, forceBetaDomain: true); + var verifyResult = await AuthApi().mobileVerify(result.domain!, forceBetaDomain: true); var authCode = await _getAuthCode(result, verifyResult); - var token = await _getToken(result, verifyResult, authCode); + var token = await _getToken(result, verifyResult, authCode!); result = result.rebuild((b) => b..token = token); return result; @@ -83,19 +83,19 @@ class UserSeedApi { } // Get the token for the SeededUser, given MobileVerifyResult and authCode - static Future _getToken(SeededUser user, MobileVerifyResult verifyResult, String authCode) async { - var dio = seedingDio(baseUrl: "https://${user.domain}/"); + static Future _getToken(SeededUser user, MobileVerifyResult? verifyResult, String authCode) async { + var dio = await seedingDio(baseUrl: "https://${user.domain}/"); var response = await dio.post('login/oauth2/token', queryParameters: { - "client_id": verifyResult.clientId, - "client_secret": verifyResult.clientSecret, + "client_id": verifyResult?.clientId, + "client_secret": verifyResult?.clientSecret, "code": authCode, "redirect_uri": _REDIRECT_URI }); if (response.statusCode == 200) { var parsedResponse = deserialize(response.data); - var token = parsedResponse.accessToken; + var token = parsedResponse?.accessToken; return token; } else { @@ -106,11 +106,11 @@ class UserSeedApi { // Get the authCode for the SeededUser, using the clientId from verifyResult. // This one is a little tricky as we have to call into native Android jsoup logic. - static Future _getAuthCode(SeededUser user, MobileVerifyResult verifyResult) async { + static Future _getAuthCode(SeededUser user, MobileVerifyResult? verifyResult) async { try { var result = await authCodeChannel.invokeMethod('getAuthCode', { 'domain': user.domain, - 'clientId': verifyResult.clientId, + 'clientId': verifyResult?.clientId, 'redirectUrl': _REDIRECT_URI, 'login': user.loginId, 'password': user.password @@ -129,15 +129,16 @@ class UserSeedApi { /// Obtain a pairing code for the indicated observee. /// Will only work if the observee has been enrolled in a course as a student. /// Returns a PairingCode structure, which contains a "code" field. - static Future createObserverPairingCode(String observeeId) async { - var dio = seedingDio(); + static Future createObserverPairingCode(String observeeId) async { + var dio = await seedingDio(); return fetch(dio.post('users/$observeeId/observer_pairing_codes')); } /// Add [observer] as an observer for [observee], using the indicated pairingCode. - static Future addObservee(SeededUser observer, SeededUser observee, String pairingCode) async { + static Future addObservee(SeededUser observer, SeededUser observee, String? pairingCode) async { try { - var pairingResponse = await seedingDio().post('users/${observer.id}/observees', + var dio = await seedingDio(); + var pairingResponse = await dio.post('users/${observer.id}/observees', queryParameters: {'pairing_code': pairingCode, 'access_token': observee.token}); return (pairingResponse.statusCode == 200 || pairingResponse.statusCode == 201); } on DioError { diff --git a/apps/flutter_parent/test_driver/app_seed_utils.dart b/apps/flutter_parent/test_driver/app_seed_utils.dart index 8dab5d4499..f32244eed6 100644 --- a/apps/flutter_parent/test_driver/app_seed_utils.dart +++ b/apps/flutter_parent/test_driver/app_seed_utils.dart @@ -43,10 +43,11 @@ class AppSeedUtils { ..build()); // The listener/handler to pass to enableFlutterDriverExtension() - static DataHandler seedContextListener = (String message) async { + static DataHandler seedContextListener = (String? message) async { if (message == "GetSeedContext") { return json.encode(serialize(_seedContext)); } + return ''; }; // Lets the test driver know that data seeding has completed. @@ -67,24 +68,24 @@ class AppSeedUtils { // Create nParents parents for (int i = 0; i < nParents; i++) { - result.parents.add(await UserSeedApi.createUser()); + result.parents.add((await UserSeedApi.createUser())!); } // Create a single teacher - result.teachers.add(await UserSeedApi.createUser()); + result.teachers.add((await UserSeedApi.createUser())!); // Create nStudents students for (int i = 0; i < nStudents; i++) { var newStudent = await UserSeedApi.createUser(); - result.students.add(newStudent); + result.students.add(newStudent!); } // Enroll all students and teachers in all courses. for (int i = 0; i < nCourses; i++) { - var newCourse = await CourseSeedApi.createCourse(); + var newCourse = (await CourseSeedApi.createCourse())!; result.courses.add(newCourse); - await EnrollmentSeedApi.createEnrollment(result.teachers.first.id, newCourse.id, "TeacherEnrollment", ""); + await EnrollmentSeedApi.createEnrollment(result.teachers.first!.id, newCourse.id, "TeacherEnrollment", ""); for (int i = 0; i < result.students.length; i++) { await EnrollmentSeedApi.createEnrollment( result.students.elementAt(i).id, newCourse.id, "StudentEnrollment", ""); @@ -106,8 +107,8 @@ class AppSeedUtils { /// Allows you a little more flexibility in setting up a course / enrollment than is allowed by /// seed() above. static Future seedCourseAndEnrollments( - {SeededUser parent = null, SeededUser student = null, SeededUser teacher = null}) async { - var newCourse = await CourseSeedApi.createCourse(); + {SeededUser? parent = null, SeededUser? student = null, SeededUser? teacher = null}) async { + var newCourse = (await CourseSeedApi.createCourse())!; if (parent != null && student != null) { await EnrollmentSeedApi.createEnrollment(parent.id, newCourse.id, "ObserverEnrollment", student.id); @@ -127,7 +128,7 @@ class AppSeedUtils { /// Pair a parent and a student. Will only work if student is enrolled as a student. static Future seedPairing(SeededUser parent, SeededUser student) async { var pairingCodeStructure = await UserSeedApi.createObserverPairingCode(student.id); - var pairingCode = pairingCodeStructure.code; + var pairingCode = pairingCodeStructure?.code; var pairingResult = await UserSeedApi.addObservee(parent, student, pairingCode); return pairingResult; } diff --git a/apps/flutter_parent/test_driver/calendar.dart b/apps/flutter_parent/test_driver/calendar.dart index e0fedcc722..050732c640 100644 --- a/apps/flutter_parent/test_driver/calendar.dart +++ b/apps/flutter_parent/test_driver/calendar.dart @@ -17,6 +17,8 @@ import 'dart:convert'; import 'package:built_collection/built_collection.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_parent/main.dart' as app; +import 'package:flutter_parent/models/assignment.dart'; +import 'package:flutter_parent/models/schedule_item.dart'; import 'package:flutter_parent/models/serializers.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; @@ -36,12 +38,9 @@ void main() async { var student = data.students[0]; var course1 = data.courses[0]; var course2 = data.courses[1]; - var assignment1 = - await AssignmentSeedApi.createAssignment(course1.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()); - var assignment2 = - await AssignmentSeedApi.createAssignment(course2.id, dueAt: DateTime.now().subtract(Duration(days: 1)).toUtc()); - var event2 = await CalendarSeedApi.createCalendarEvent(course2.id, "Calendar Event", DateTime.now().toUtc(), - allDay: true, locationName: "Location Name", locationAddress: "Location Address"); + Assignment assignment1 = (await AssignmentSeedApi.createAssignment(course1.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()))!; + Assignment assignment2 = (await AssignmentSeedApi.createAssignment(course2.id, dueAt: DateTime.now().subtract(Duration(days: 1)).toUtc()))!; + ScheduleItem event2 = (await CalendarSeedApi.createCalendarEvent(course2.id, "Calendar Event", DateTime.now().toUtc(), allDay: true, locationName: "Location Name", locationAddress: "Location Address"))!; // TODO: Add graded quiz // Sign in the parent diff --git a/apps/flutter_parent/test_driver/calendar_test.dart b/apps/flutter_parent/test_driver/calendar_test.dart index 8f6552b467..24b2c5e3ce 100644 --- a/apps/flutter_parent/test_driver/calendar_test.dart +++ b/apps/flutter_parent/test_driver/calendar_test.dart @@ -26,8 +26,9 @@ import 'pages/assignment_details_page.dart'; import 'pages/calendar_page.dart'; import 'pages/dashboard_page.dart'; +// Run test with command: flutter drive --target=test_driver/calendar.dart void main() { - FlutterDriver driver; + FlutterDriver? driver; // Connect to the Flutter driver before running any tests. setUpAll(() async { @@ -37,7 +38,7 @@ void main() { // Close the connection to the driver after the tests have completed. tearDownAll(() async { if (driver != null) { - driver.close(); + driver?.close(); } }); @@ -52,15 +53,15 @@ void main() { // as we usually run the test M-F. test('Calendar E2E', () async { // Wait for seeding to complete - var seedContext = await DriverSeedUtils.waitForSeedingToComplete(driver); + var seedContext = (await DriverSeedUtils.waitForSeedingToComplete(driver))!; print("driver: Seeding complete!"); var parent = seedContext.getNamedObject("parent"); var student = seedContext.getNamedObject("student"); - var courses = [seedContext.getNamedObject("course1"), seedContext.getNamedObject("course2")]; - var assignment1 = seedContext.getNamedObject("assignment1"); // From first course - var assignment2 = seedContext.getNamedObject("assignment2"); // From second course - var event2 = seedContext.getNamedObject("event2"); // From second course + var courses = [seedContext.getNamedObject("course1")!, seedContext.getNamedObject("course2")!]; + var assignment1 = seedContext.getNamedObject("assignment1")!; // From first course + var assignment2 = seedContext.getNamedObject("assignment2")!; // From second course + var event2 = seedContext.getNamedObject("event2")!; // From second course // Let's check that all of our assignments, quizzes and announcements are displayed await DashboardPage.waitForRender(driver); @@ -73,7 +74,7 @@ void main() { // Let's try opening an assignment await CalendarPage.openAssignment(driver, assignment1); await AssignmentDetailsPage.validateUnsubmittedAssignment(driver, assignment1); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); // Let's filter out the first course and try again await CalendarPage.toggleFilter(driver, courses[0]); diff --git a/apps/flutter_parent/test_driver/dashboard_test.dart b/apps/flutter_parent/test_driver/dashboard_test.dart index b9651ee89f..aa1166bd22 100644 --- a/apps/flutter_parent/test_driver/dashboard_test.dart +++ b/apps/flutter_parent/test_driver/dashboard_test.dart @@ -23,7 +23,7 @@ import 'driver_seed_utils.dart'; import 'pages/dashboard_page.dart'; void main() { - FlutterDriver driver; + FlutterDriver? driver; // Connect to the Flutter driver before running any tests. setUpAll(() async { @@ -33,21 +33,21 @@ void main() { // Close the connection to the driver after the tests have completed. tearDownAll(() async { if (driver != null) { - driver.close(); + driver?.close(); } }); test('Dashboard E2E', () async { // Wait for seeding to complete - var seedContext = await DriverSeedUtils.waitForSeedingToComplete(driver); + var seedContext = (await DriverSeedUtils.waitForSeedingToComplete(driver))!; print("driver: Seeding complete!"); var students = [ - seedContext.getNamedObject("student1"), - seedContext.getNamedObject("student2") + seedContext.getNamedObject("student1")!, + seedContext.getNamedObject("student2")! ]; - var courses = [seedContext.getNamedObject("course1"), seedContext.getNamedObject("course2")]; - var parent = seedContext.getNamedObject("parent"); + var courses = [seedContext.getNamedObject("course1")!, seedContext.getNamedObject("course2")!]; + var parent = seedContext.getNamedObject("parent")!; await DashboardPage.waitForRender(driver); @@ -67,6 +67,6 @@ void main() { await DashboardPage.openNavDrawer(driver); // And the name of our parent is displayed - await driver.waitFor(find.text(parent.name)); + await driver?.waitFor(find.text(parent.name)); }, timeout: Timeout(Duration(minutes: 1))); // Change timeout from 30 sec default to 1 min } diff --git a/apps/flutter_parent/test_driver/driver_seed_utils.dart b/apps/flutter_parent/test_driver/driver_seed_utils.dart index de9ea65f0b..fbdc1d1f27 100644 --- a/apps/flutter_parent/test_driver/driver_seed_utils.dart +++ b/apps/flutter_parent/test_driver/driver_seed_utils.dart @@ -21,15 +21,15 @@ import 'package:flutter_parent/models/serializers.dart'; // Some driver-side abstractions for grabbing SeedContext from the app. class DriverSeedUtils { // Driver side: Grab the current seed context (might be incomplete). - static Future _getSeedContext(FlutterDriver driver) async { - var jsonContext = await driver.requestData("GetSeedContext"); - return deserialize(json.decode(jsonContext)); + static Future _getSeedContext(FlutterDriver? driver) async { + var jsonContext = await driver?.requestData("GetSeedContext"); + return deserialize(json.decode(jsonContext ?? ''))!; } // Driver side: Retrieve the SeedContext once seeding is complete - static Future waitForSeedingToComplete(FlutterDriver driver) async { + static Future waitForSeedingToComplete(FlutterDriver? driver) async { var seedContext = await _getSeedContext(driver); - while (!seedContext.seedingComplete) { + while (seedContext.seedingComplete == false) { await Future.delayed(const Duration(seconds: 1)); seedContext = await (_getSeedContext(driver)); } diff --git a/apps/flutter_parent/test_driver/flutter_driver_extensions.dart b/apps/flutter_parent/test_driver/flutter_driver_extensions.dart index af1ef8a104..12bb8f11c5 100644 --- a/apps/flutter_parent/test_driver/flutter_driver_extensions.dart +++ b/apps/flutter_parent/test_driver/flutter_driver_extensions.dart @@ -18,7 +18,7 @@ import 'package:flutter_driver/flutter_driver.dart'; // for some operations. extension AutoRefresh on FlutterDriver { Future getTextWithRefreshes(SerializableFinder finder, - {int refreshes = 3, String expectedText = null}) async { + {int refreshes = 3, String? expectedText = null}) async { for (int i = 0; i < refreshes; i++) { try { var result = await this.getText(finder, timeout: Duration(seconds: 1)); diff --git a/apps/flutter_parent/test_driver/grades_assignments.dart b/apps/flutter_parent/test_driver/grades_assignments.dart index bd30bb90b1..f241fa2004 100644 --- a/apps/flutter_parent/test_driver/grades_assignments.dart +++ b/apps/flutter_parent/test_driver/grades_assignments.dart @@ -21,23 +21,19 @@ void main() async { var course = data.courses[0]; var student = data.students[0]; // past-due - var assignment1 = - await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().subtract(Duration(days: 1)).toUtc()); + var assignment1 = (await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().subtract(Duration(days: 1)).toUtc()))!; // Unsubmitted - var assignment2 = - await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()); + var assignment2 = (await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()))!; // Submitted - var assignment3 = - await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()); + var assignment3 = (await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()))!; await Future.delayed(const Duration(seconds: 2)); // Allow some time for assignment-creation delayed jobs to complete - var submission3 = await SubmissionSeedApi.createSubmission(course.id, assignment3, student.id); + var submission3 = (await SubmissionSeedApi.createSubmission(course.id, assignment3, student.id))!; // Graded - var assignment4 = - await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 3)).toUtc()); + var assignment4 = (await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 3)).toUtc()))!; await Future.delayed(const Duration(seconds: 2)); // Allow some time for assignment-creation delayed jobs to complete - var submission4 = await SubmissionSeedApi.createSubmission(course.id, assignment4, student.id); - var grade4 = await SubmissionSeedApi.gradeSubmission(course.id, assignment4, student.id, "19"); + var submission4 = (await SubmissionSeedApi.createSubmission(course.id, assignment4, student.id))!; + var grade4 = (await SubmissionSeedApi.gradeSubmission(course.id, assignment4, student.id, "19"))!; // Sign in the parent await AppSeedUtils.signIn(data.parents.first); diff --git a/apps/flutter_parent/test_driver/grades_assignments_test.dart b/apps/flutter_parent/test_driver/grades_assignments_test.dart index 700afa15ae..1109ab50b8 100644 --- a/apps/flutter_parent/test_driver/grades_assignments_test.dart +++ b/apps/flutter_parent/test_driver/grades_assignments_test.dart @@ -25,7 +25,7 @@ import 'pages/course_grades_page.dart'; import 'pages/dashboard_page.dart'; void main() { - FlutterDriver driver; + FlutterDriver? driver; // Connect to the Flutter driver before running any tests. setUpAll(() async { @@ -35,21 +35,21 @@ void main() { // Close the connection to the driver after the tests have completed. tearDownAll(() async { if (driver != null) { - driver.close(); + driver?.close(); } }); test('Grades+Assignments E2E', () async { // Wait for seeding to complete - var seedContext = await DriverSeedUtils.waitForSeedingToComplete(driver); + var seedContext = (await DriverSeedUtils.waitForSeedingToComplete(driver))!; print("driver: Seeding complete!"); - var course = seedContext.getNamedObject("course"); + var course = seedContext.getNamedObject("course")!; var assignments = [ - seedContext.getNamedObject("assignment1"), - seedContext.getNamedObject("assignment2"), - seedContext.getNamedObject("assignment3"), - seedContext.getNamedObject("assignment4") + seedContext.getNamedObject("assignment1")!, + seedContext.getNamedObject("assignment2")!, + seedContext.getNamedObject("assignment3")!, + seedContext.getNamedObject("assignment4")! ]; // Assignment-specific data @@ -79,18 +79,18 @@ void main() { // For each assignment, open the assignment details page and verify its correctness await CourseGradesPage.selectAssignment(driver, assignments[0]); await AssignmentDetailsPage.validateUnsubmittedAssignment(driver, assignments[0]); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); await CourseGradesPage.selectAssignment(driver, assignments[1]); await AssignmentDetailsPage.validateUnsubmittedAssignment(driver, assignments[1]); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); await CourseGradesPage.selectAssignment(driver, assignments[2]); await AssignmentDetailsPage.validateSubmittedAssignment(driver, assignments[2]); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); await CourseGradesPage.selectAssignment(driver, assignments[3]); await AssignmentDetailsPage.validateGradedAssignment(driver, assignments[3], "19"); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); }, timeout: Timeout(Duration(minutes: 2))); } diff --git a/apps/flutter_parent/test_driver/inbox.dart b/apps/flutter_parent/test_driver/inbox.dart index d7f52ad466..8d0746558b 100644 --- a/apps/flutter_parent/test_driver/inbox.dart +++ b/apps/flutter_parent/test_driver/inbox.dart @@ -39,15 +39,14 @@ void main() async { // Seed an assignment // Graded - var assignment = - await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 3)).toUtc()); + var assignment = (await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 3)).toUtc()))!; // Sign in the parent await AppSeedUtils.signIn(parent); // Seed a conversation await Future.delayed(const Duration(seconds: 5)); - var newConversation = await InboxApi().createConversation(course.id, [teacher.id], "sUp?", "Let's talk", null); + var newConversation = (await InboxApi().createConversation(course.id, [teacher.id], "sUp?", "Let's talk", null))!; newConversation = newConversation.rebuild((b) => b..contextName = course.name); // Do this manually // Let the test driver know that seeding has completed diff --git a/apps/flutter_parent/test_driver/inbox_test.dart b/apps/flutter_parent/test_driver/inbox_test.dart index 4506026899..db9adabb88 100644 --- a/apps/flutter_parent/test_driver/inbox_test.dart +++ b/apps/flutter_parent/test_driver/inbox_test.dart @@ -32,7 +32,7 @@ import 'pages/course_grades_page.dart'; import 'pages/dashboard_page.dart'; void main() { - FlutterDriver driver; + FlutterDriver? driver; // Connect to the Flutter driver before running any tests. setUpAll(() async { @@ -42,29 +42,29 @@ void main() { // Close the connection to the driver after the tests have completed. tearDownAll(() async { if (driver != null) { - driver.close(); + driver?.close(); } }); test('Inbox E2E', () async { // Wait for seeding to complete - var seedContext = await DriverSeedUtils.waitForSeedingToComplete(driver); + var seedContext = (await DriverSeedUtils.waitForSeedingToComplete(driver))!; print("driver: Seeding complete!"); - var parent = seedContext.getNamedObject("parent"); - var student = seedContext.getNamedObject("student"); - var course = seedContext.getNamedObject("course"); - var teacher = seedContext.getNamedObject("teacher"); - var conversation = seedContext.getNamedObject("conversation"); - var assignment = seedContext.getNamedObject("assignment"); + var parent = seedContext.getNamedObject("parent")!; + var student = seedContext.getNamedObject("student")!; + var course = seedContext.getNamedObject("course")!; + var teacher = seedContext.getNamedObject("teacher")!; + var conversation = seedContext.getNamedObject("conversation")!; + var assignment = seedContext.getNamedObject("assignment")!; // Verify that the pre-seeded conversation shows up await DashboardPage.waitForRender(driver); await DashboardPage.openInbox(driver); await ConversationListPage.verifyConversationDataDisplayed(driver, /*index*/ 0, partialSubjects: [conversation.subject], - partialContexts: [conversation.contextName], - partialBodies: [conversation.lastMessage ?? conversation.lastAuthoredMessage]); + partialContexts: [conversation.contextName!], + partialBodies: [conversation.lastMessage ?? conversation.lastAuthoredMessage!]); // Create a conversation from the Inbox await ConversationListPage.initiateCreateEmail(driver, course); // Will this work with only one course? @@ -78,7 +78,7 @@ void main() { partialBodies: ['Message 1 Body'], partialSubjects: [course.name]); // Back to the dashboard - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); // Select a course and send a grades-related email await DashboardPage.selectCourse(driver, course); @@ -106,11 +106,11 @@ void main() { await ConversationCreatePage.populateBody(driver, 'Assignment Body'); await ConversationCreatePage.sendMail(driver); - await driver.tap(find.pageBack()); // assignment details -> grades list - await driver.tap(find.pageBack()); // grades list -> dashboard + await driver?.tap(find.pageBack()); // assignment details -> grades list + await driver?.tap(find.pageBack()); // grades list -> dashboard await DashboardPage.openInbox(driver); - await driver.refresh(); // To make sure and load the latest emails + await driver?.refresh(); // To make sure and load the latest emails await Future.delayed(const Duration(seconds: 2)); // Give ourselves a moment to load // Let's make sure that the three most recent emails that we created show up in our @@ -118,7 +118,7 @@ void main() { // The most recent email -- the assignment email -- should be on top (index 0) await ConversationListPage.verifyConversationDataDisplayed(driver, 0, - partialSubjects: [student.name, assignment.name], + partialSubjects: [student.name, assignment.name!], partialContexts: [course.name], partialBodies: ['Assignment Body', student.name]); @@ -149,8 +149,8 @@ void main() { await ConversationCreatePage.sendMail(driver); // And make sure that our new message shows up on the conversation list page... - await driver.tap(find.pageBack()); // From conversation detail page to conversation list page - await driver.refresh(); + await driver?.tap(find.pageBack()); // From conversation detail page to conversation list page + await driver?.refresh(); await ConversationListPage.verifyConversationDataDisplayed(driver, 0, partialBodies: ['reply body']); }, timeout: Timeout(Duration(minutes: 2))); } diff --git a/apps/flutter_parent/test_driver/manage_students.dart b/apps/flutter_parent/test_driver/manage_students.dart index 58d064c9c6..1a57daefc5 100644 --- a/apps/flutter_parent/test_driver/manage_students.dart +++ b/apps/flutter_parent/test_driver/manage_students.dart @@ -30,17 +30,17 @@ void main() async { await ApiPrefs.init(); // Create parent and two students, one of which is paired to the parent. - var parent = await UserSeedApi.createUser(); - var student1 = await UserSeedApi.createUser(); - var student2 = await UserSeedApi.createUser(); - var teacher = await UserSeedApi.createUser(); + var parent = (await UserSeedApi.createUser())!; + var student1 = (await UserSeedApi.createUser())!; + var student2 = (await UserSeedApi.createUser())!; + var teacher = (await UserSeedApi.createUser())!; var course1 = await AppSeedUtils.seedCourseAndEnrollments(student: student1, teacher: teacher); var course2 = await AppSeedUtils.seedCourseAndEnrollments(student: student2, teacher: teacher); await AppSeedUtils.seedPairing(parent, student1); // Get a pairing code for student2 var pairingCodeStructure = await UserSeedApi.createObserverPairingCode(student2.id); - var pairingCode = pairingCodeStructure.code; + var pairingCode = pairingCodeStructure?.code; print("PAIRING CODE: $pairingCode"); // Sign in the parent diff --git a/apps/flutter_parent/test_driver/manage_students_test.dart b/apps/flutter_parent/test_driver/manage_students_test.dart index 3b7bcf73a7..038bd29338 100644 --- a/apps/flutter_parent/test_driver/manage_students_test.dart +++ b/apps/flutter_parent/test_driver/manage_students_test.dart @@ -24,7 +24,7 @@ import 'pages/dashboard_page.dart'; import 'pages/manage_students_page.dart'; void main() { - FlutterDriver driver; + FlutterDriver? driver; // Connect to the Flutter driver before running any tests. setUpAll(() async { @@ -34,7 +34,7 @@ void main() { // Close the connection to the driver after the tests have completed. tearDownAll(() async { if (driver != null) { - driver.close(); + driver?.close(); } }); @@ -42,21 +42,21 @@ void main() { // Does NOT test anything related to alerts or alert settings; that will be the subject of another test. test('Manage Students E2E', () async { // Wait for seeding to complete - var seedContext = await DriverSeedUtils.waitForSeedingToComplete(driver); + var seedContext = (await DriverSeedUtils.waitForSeedingToComplete(driver))!; print("driver: Seeding complete!"); // Read in our seeded data var students = [ - seedContext.getNamedObject("student1"), - seedContext.getNamedObject("student2") + seedContext.getNamedObject("student1")!, + seedContext.getNamedObject("student2")! ]; var courses = [ - seedContext.getNamedObject("course1"), - seedContext.getNamedObject("course2"), + seedContext.getNamedObject("course1")!, + seedContext.getNamedObject("course2")!, ]; - var parent = seedContext.getNamedObject("parent"); - var pairingCode = seedContext.seedObjects["pairingCode2"]; // Direct string fetch + var parent = seedContext.getNamedObject("parent")!; + var pairingCode = seedContext.seedObjects["pairingCode2"]!; // Direct string fetch // Verify that student[0] and course[0] show up on the main dashboard page await DashboardPage.waitForRender(driver); @@ -77,7 +77,7 @@ void main() { await ManageStudentsPage.verifyStudentDisplayed(driver, students[1]); // Back to main dashboard - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); // Switch students and verify that new student's course is showing await DashboardPage.changeStudent(driver, students[1]); diff --git a/apps/flutter_parent/test_driver/pages/assignment_details_page.dart b/apps/flutter_parent/test_driver/pages/assignment_details_page.dart index 3d8008de48..145d3b7837 100644 --- a/apps/flutter_parent/test_driver/pages/assignment_details_page.dart +++ b/apps/flutter_parent/test_driver/pages/assignment_details_page.dart @@ -20,80 +20,80 @@ import 'package:intl/intl.dart'; import 'package:test/test.dart'; class AssignmentDetailsPage { - static Future validateGradedAssignment(FlutterDriver driver, Assignment assignment, String grade) async { - await driver.waitFor(find.text(assignment.name)); // No key to use here + static Future validateGradedAssignment(FlutterDriver? driver, Assignment assignment, String grade) async { + await driver?.waitFor(find.text(assignment.name!)); // No key to use here - var pointTotalText = await driver.getText(find.byValueKey("assignment_details_total_points")); + var pointTotalText = await driver?.getText(find.byValueKey("assignment_details_total_points")); var pointTotalExpected = assignment.pointsPossible.toInt().toString(); - expect(pointTotalText.contains(pointTotalExpected), true, + expect(pointTotalText?.contains(pointTotalExpected), true, reason: "Expected total points to include $pointTotalExpected"); - var statusText = await driver.getText(find.byValueKey("assignment_details_status")); + var statusText = await driver?.getText(find.byValueKey("assignment_details_status")); expect(statusText, "Graded", reason: "Expected status to be Graded"); - _validateDueDate(driver, assignment.dueAt); + _validateDueDate(driver, assignment.dueAt!); - await driver.scrollIntoView(find.byValueKey('grade-cell-graded-container')); - var gradeText = await driver.getText(find.byValueKey('grade-cell-score')); + await driver?.scrollIntoView(find.byValueKey('grade-cell-graded-container')); + var gradeText = await driver?.getText(find.byValueKey('grade-cell-score')); expect(gradeText, grade, reason: "Expected grade to be $grade"); } - static Future validateSubmittedAssignment(FlutterDriver driver, Assignment assignment) async { - await driver.waitFor(find.text(assignment.name)); // No key to use here + static Future validateSubmittedAssignment(FlutterDriver? driver, Assignment assignment) async { + await driver?.waitFor(find.text(assignment.name!)); // No key to use here - var pointTotalText = await driver.getText(find.byValueKey("assignment_details_total_points")); + var pointTotalText = await driver?.getText(find.byValueKey("assignment_details_total_points")); var pointTotalExpected = assignment.pointsPossible.toInt().toString(); - expect(pointTotalText.contains(pointTotalExpected), true, + expect(pointTotalText?.contains(pointTotalExpected), true, reason: "Expected total points to include $pointTotalExpected"); - var statusText = await driver.getText(find.byValueKey("assignment_details_status")); + var statusText = await driver?.getText(find.byValueKey("assignment_details_status")); expect(statusText, "Submitted", reason: "Expected status to be Submitted"); - _validateDueDate(driver, assignment.dueAt); + _validateDueDate(driver, assignment.dueAt!); - await driver.scrollIntoView(find.byValueKey('grade-cell-submitted-container')); - var submittedStatus = await driver.getText(find.byValueKey('grade-cell-submit-status')); + await driver?.scrollIntoView(find.byValueKey('grade-cell-submitted-container')); + var submittedStatus = await driver?.getText(find.byValueKey('grade-cell-submit-status')); expect(submittedStatus, "Successfully submitted!", reason: "Expected to see 'Successfully submitted!'"); } - static Future validateUnsubmittedAssignment(FlutterDriver driver, Assignment assignment) async { - await driver.waitFor(find.text(assignment.name)); // No key to use here + static Future validateUnsubmittedAssignment(FlutterDriver? driver, Assignment assignment) async { + await driver?.waitFor(find.text(assignment.name!)); // No key to use here - var pointTotalText = await driver.getText(find.byValueKey("assignment_details_total_points")); + var pointTotalText = await driver?.getText(find.byValueKey("assignment_details_total_points")); var pointTotalExpected = assignment.pointsPossible.toInt().toString(); - expect(pointTotalText.contains(pointTotalExpected), true, + expect(pointTotalText?.contains(pointTotalExpected), true, reason: "Expected total points to include $pointTotalExpected"); - var statusText = await driver.getText(find.byValueKey("assignment_details_status")); + var statusText = await driver?.getText(find.byValueKey("assignment_details_status")); expect(statusText, "Not Submitted", reason: "Expected status to be Not Submitted"); - _validateDueDate(driver, assignment.dueAt); + _validateDueDate(driver, assignment.dueAt!); } - static Future validateUnsubmittedQuiz(FlutterDriver driver, Quiz quiz) async { - await driver.waitFor(find.text(quiz.title)); // No key to use here + static Future validateUnsubmittedQuiz(FlutterDriver? driver, Quiz quiz) async { + await driver?.waitFor(find.text(quiz.title)); // No key to use here - var pointTotalText = await driver.getText(find.byValueKey("assignment_details_total_points")); + var pointTotalText = await driver?.getText(find.byValueKey("assignment_details_total_points")); var pointTotalExpected = quiz.pointsPossible.toInt().toString(); - expect(pointTotalText.contains(pointTotalExpected), true, + expect(pointTotalText?.contains(pointTotalExpected), true, reason: "Expected total points to include $pointTotalExpected"); - var statusText = await driver.getText(find.byValueKey("assignment_details_status")); + var statusText = await driver?.getText(find.byValueKey("assignment_details_status")); expect(statusText, "Not Submitted", reason: "Expected status to be Not Submitted"); _validateDueDate(driver, quiz.dueAt); } - static Future _validateDueDate(FlutterDriver driver, DateTime dueAt) async { + static Future _validateDueDate(FlutterDriver? driver, DateTime dueAt) async { var localDate = dueAt.toLocal(); - String date = (DateFormat.MMMd(supportedDateLocale)).format(localDate); - String time = (DateFormat.jm(supportedDateLocale)).format(localDate); - var dueDateText = await driver.getText(find.byValueKey("assignment_details_due_date")); - expect(dueDateText.contains(date), true, reason: "Expected due date to contain $date"); - expect(dueDateText.contains(time), true, reason: "Expected due date to contain $time"); + String date = (DateFormat.MMMd(supportedDateLocale)).format(localDate).replaceAll(RegExp('[^A-Za-z0-9]'), ''); + String time = (DateFormat.jm(supportedDateLocale)).format(localDate).replaceAll(RegExp('[^A-Za-z0-9]'), ''); + var dueDateText = (await driver?.getText(find.byValueKey("assignment_details_due_date")))!.replaceAll(RegExp('[^A-Za-z0-9]'), ''); + expect(dueDateText.contains(date), true, reason: "Expected due date to contain $date [$dueDateText]"); + expect(dueDateText.contains(time), true, reason: "Expected due date to contain $time [$dueDateText]"); } - static Future initiateCreateEmail(FlutterDriver driver) async { - await driver.tap(find.byType("FloatingActionButton")); + static Future initiateCreateEmail(FlutterDriver? driver) async { + await driver?.tap(find.byType("FloatingActionButton")); } } diff --git a/apps/flutter_parent/test_driver/pages/calendar_page.dart b/apps/flutter_parent/test_driver/pages/calendar_page.dart index bb567c17cf..7fdb06ea62 100644 --- a/apps/flutter_parent/test_driver/pages/calendar_page.dart +++ b/apps/flutter_parent/test_driver/pages/calendar_page.dart @@ -9,95 +9,95 @@ import 'package:test/test.dart'; import '../flutter_driver_extensions.dart'; class CalendarPage { - static Future waitForRender(FlutterDriver driver) async { + static Future waitForRender(FlutterDriver? driver) async { var dayOfMonth = DateTime.now().toLocal().day; - await driver.waitFor(find.byValueKey('day_of_month_$dayOfMonth'), timeout: Duration(milliseconds: 9998)); + await driver?.waitFor(find.byValueKey('day_of_month_$dayOfMonth'), timeout: Duration(milliseconds: 9998)); } - static Future verifyAnnouncementDisplayed(FlutterDriver driver, Announcement announcement) async { + static Future verifyAnnouncementDisplayed(FlutterDriver? driver, Announcement announcement) async { await _presentHelper(driver, announcement.postedAt, announcement.title); } - static Future verifyAnnouncementNotDisplayed(FlutterDriver driver, Announcement announcement) async { + static Future verifyAnnouncementNotDisplayed(FlutterDriver? driver, Announcement announcement) async { await _absentHelper(driver, announcement.postedAt, announcement.title); } - static Future verifyAssignmentDisplayed(FlutterDriver driver, Assignment assignment) async { - await _presentHelper(driver, assignment.dueAt, assignment.name); + static Future verifyAssignmentDisplayed(FlutterDriver? driver, Assignment assignment) async { + await _presentHelper(driver, assignment.dueAt!, assignment.name!); } - static Future verifyAssignmentNotDisplayed(FlutterDriver driver, Assignment assignment) async { - await _absentHelper(driver, assignment.dueAt, assignment.name); + static Future verifyAssignmentNotDisplayed(FlutterDriver? driver, Assignment assignment) async { + await _absentHelper(driver, assignment.dueAt!, assignment.name!); } - static Future verifyQuizDisplayed(FlutterDriver driver, Quiz quiz) async { + static Future verifyQuizDisplayed(FlutterDriver? driver, Quiz quiz) async { await _presentHelper(driver, quiz.dueAt, quiz.title); } - static Future verifyQuizNotDisplayed(FlutterDriver driver, Quiz quiz) async { + static Future verifyQuizNotDisplayed(FlutterDriver? driver, Quiz quiz) async { await _absentHelper(driver, quiz.dueAt, quiz.title); } - static Future verifyEventDisplayed(FlutterDriver driver, ScheduleItem event) async { - await _presentHelper(driver, event.isAllDay ? event.allDayDate : event.startAt, event.title); + static Future verifyEventDisplayed(FlutterDriver? driver, ScheduleItem event) async { + await _presentHelper(driver, event.isAllDay ? event.allDayDate! : event.startAt!, event.title!); } - static Future verifyEventNotDisplayed(FlutterDriver driver, ScheduleItem event) async { - await _absentHelper(driver, event.isAllDay ? event.allDayDate : event.startAt, event.title); + static Future verifyEventNotDisplayed(FlutterDriver? driver, ScheduleItem event) async { + await _absentHelper(driver, event.isAllDay ? event.allDayDate! : event.startAt!, event.title!); } - static Future toggleFilter(FlutterDriver driver, Course course) async { - await driver.tap(find.text("Calendars")); - await driver.tap(find.text(course.name)); - await driver.tap(find.pageBack()); - await driver.waitForAbsent(find.byType('LoadingIndicator'), timeout: Duration(milliseconds: 4999)); + static Future toggleFilter(FlutterDriver? driver, Course course) async { + await driver?.tap(find.text("Calendars")); + await driver?.tap(find.text(course.name)); + await driver?.tap(find.pageBack()); + await driver?.waitForAbsent(find.byType('LoadingIndicator'), timeout: Duration(milliseconds: 4999)); } - static Future openAssignment(FlutterDriver driver, Assignment assignment) async { + static Future openAssignment(FlutterDriver? driver, Assignment assignment) async { // This should get the assignment into view - await _presentHelper(driver, assignment.dueAt, assignment.name); - await driver.tap(find.text(assignment.name)); + await _presentHelper(driver, assignment.dueAt!, assignment.name!); + await driver?.tap(find.text(assignment.name!)); } // Helper function to (1) scroll to the correct week if necessary, (2) select the date, and // (3) make sure that the search text is present. - static Future _presentHelper(FlutterDriver driver, DateTime displayDate, String searchString) async { + static Future _presentHelper(FlutterDriver? driver, DateTime displayDate, String searchString) async { //print('_presentHelper($displayDate,$searchString)'); var dayOfMonth = displayDate.toLocal().day; var present = await _scrollToDayOfMonth(driver, dayOfMonth); expect(present, true, reason: "FAILED to scroll to day-of-month $dayOfMonth"); - await driver.tap(find.byValueKey('day_of_month_$dayOfMonth')); - await driver.scrollIntoView(find.text(searchString)); - await driver.waitWithRefreshes(find.text(searchString)); + await driver?.tap(find.byValueKey('day_of_month_$dayOfMonth')); + await driver?.scrollIntoView(find.text(searchString)); + await driver?.waitWithRefreshes(find.text(searchString)); } // Helper function to (1) scroll to the correct week if necessary, (2) select the date, and // (3) make sure that the search text is NOT present. - static Future _absentHelper(FlutterDriver driver, DateTime displayDate, String searchString) async { + static Future _absentHelper(FlutterDriver? driver, DateTime displayDate, String searchString) async { //print('_absentHelper($displayDate,$searchString)'); var dayOfMonth = displayDate.toLocal().day; var present = await _scrollToDayOfMonth(driver, dayOfMonth); expect(present, true, reason: "FAILED to scroll to day-of-month $dayOfMonth"); - await driver.tap(find.byValueKey('day_of_month_$dayOfMonth')); - await driver.waitForAbsentWithRefreshes(find.text(searchString)); + await driver?.tap(find.byValueKey('day_of_month_$dayOfMonth')); + await driver?.waitForAbsentWithRefreshes(find.text(searchString)); } // Helper function makes an attempt to scroll to the right week if the day of month that we are looking // for is not visible. Only searches one week forward and one week backwards. - static Future _scrollToDayOfMonth(FlutterDriver driver, int dayOfMonth) async { + static Future _scrollToDayOfMonth(FlutterDriver? driver, int dayOfMonth) async { var present = await _dayPresent(driver, dayOfMonth); if (present) return true; // Not present. Scroll one way and try again. //print("scrolling -400"); - await driver.scroll(find.byType('CalendarWeek'), -400, 0, Duration(milliseconds: 200)); + await driver?.scroll(find.byType('CalendarWeek'), -400, 0, Duration(milliseconds: 200)); present = await _dayPresent(driver, dayOfMonth); if (present) return true; // Still not present. Try scrolling back the opposite direction (twice, to skip initial page). //print("scrolling 800"); - await driver.scroll(find.byType('CalendarWeek'), 400, 0, Duration(milliseconds: 200)); - await driver.scroll(find.byType('CalendarWeek'), 400, 0, Duration(milliseconds: 200)); + await driver?.scroll(find.byType('CalendarWeek'), 400, 0, Duration(milliseconds: 200)); + await driver?.scroll(find.byType('CalendarWeek'), 400, 0, Duration(milliseconds: 200)); present = await _dayPresent(driver, dayOfMonth); // If this doesn't fix us, we'll just be hosed. @@ -105,9 +105,9 @@ class CalendarPage { } // Helper method returns true if the specified dayOfMonth is displayed - static Future _dayPresent(FlutterDriver driver, int dayOfMonth) async { + static Future _dayPresent(FlutterDriver? driver, int dayOfMonth) async { try { - await driver.waitFor(find.byValueKey('day_of_month_$dayOfMonth'), timeout: Duration(milliseconds: 1000)); + await driver?.waitFor(find.byValueKey('day_of_month_$dayOfMonth'), timeout: Duration(milliseconds: 1000)); return true; } catch (e) { return false; diff --git a/apps/flutter_parent/test_driver/pages/conversation_create_page.dart b/apps/flutter_parent/test_driver/pages/conversation_create_page.dart index 7eef0e6e98..81d2ac8066 100644 --- a/apps/flutter_parent/test_driver/pages/conversation_create_page.dart +++ b/apps/flutter_parent/test_driver/pages/conversation_create_page.dart @@ -16,27 +16,27 @@ import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_parent/models/dataseeding/seeded_user.dart'; class ConversationCreatePage { - static Future verifyRecipientListed(FlutterDriver driver, SeededUser user) async { + static Future verifyRecipientListed(FlutterDriver? driver, SeededUser user) async { var keyString = 'user_chip_${user.id}'; - await driver.waitFor(find.descendant(of: find.byValueKey(keyString), matching: find.text(user.shortName))); + await driver?.waitFor(find.descendant(of: find.byValueKey(keyString), matching: find.text(user.shortName))); } - static Future verifySubject(FlutterDriver driver, String subject) async { + static Future verifySubject(FlutterDriver? driver, String subject) async { //var text = await driver.getText(find.byValueKey('subjectText')); //expect(text, subject, reason: 'email subject text'); // Unfortunately, the stronger check above won't work because getText() does not // work on a TextField. - await driver.waitFor(find.text(subject), timeout: const Duration(seconds: 5)); + await driver?.waitFor(find.text(subject), timeout: const Duration(seconds: 5)); } - static Future populateBody(FlutterDriver driver, String body) async { - await driver.tap(find.byValueKey('messageText')); - await driver.enterText(body); + static Future populateBody(FlutterDriver? driver, String body) async { + await driver?.tap(find.byValueKey('messageText')); + await driver?.enterText(body); } - static Future sendMail(FlutterDriver driver) async { + static Future sendMail(FlutterDriver? driver) async { await Future.delayed(const Duration(seconds: 1)); // May need to wait a beat for the button to become enabled - await driver.tap(find.byValueKey('sendButton')); + await driver?.tap(find.byValueKey('sendButton')); } } diff --git a/apps/flutter_parent/test_driver/pages/conversation_details_page.dart b/apps/flutter_parent/test_driver/pages/conversation_details_page.dart index f1ea70b1f1..de033ae2b7 100644 --- a/apps/flutter_parent/test_driver/pages/conversation_details_page.dart +++ b/apps/flutter_parent/test_driver/pages/conversation_details_page.dart @@ -3,34 +3,34 @@ import 'package:flutter_parent/models/dataseeding/seeded_user.dart'; import 'package:test/test.dart'; class ConversationDetailsPage { - static Future verifyRecipientListed(FlutterDriver driver, int index, SeededUser user) async { + static Future verifyRecipientListed(FlutterDriver? driver, int index, SeededUser user) async { var messageFinder = find.byValueKey('conversation_message_index_$index'); var participantFinder = find.descendant(of: messageFinder, matching: find.byValueKey('participant_id_${user.id}')); - var fullText = await driver.getText(participantFinder); - expect(fullText.contains(user.shortName), true, + var fullText = await driver?.getText(participantFinder); + expect(fullText?.contains(user.shortName), true, reason: 'email detail user: searching for \"${user.shortName}\" in \"$fullText\"'); } - static Future verifySubject(FlutterDriver driver, List partialSubjects) async { - var fullText = await driver.getText(find.byValueKey('subjectText')); + static Future verifySubject(FlutterDriver? driver, List partialSubjects) async { + var fullText = await driver?.getText(find.byValueKey('subjectText')); for (String partialSubject in partialSubjects) { - expect(fullText.toLowerCase().contains(partialSubject.toLowerCase()), true, + expect(fullText?.toLowerCase().contains(partialSubject.toLowerCase()), true, reason: 'email detail header subject: searching for \"$partialSubject\" in \"$fullText\"'); } } - static Future verifyCourse(FlutterDriver driver, String courseName) async { - var fullText = await driver.getText(find.byValueKey('courseText')); - expect(fullText.contains(courseName), true, + static Future verifyCourse(FlutterDriver? driver, String courseName) async { + var fullText = await driver?.getText(find.byValueKey('courseText')); + expect(fullText?.contains(courseName), true, reason: 'email detail header course: searching for \"$courseName\" in \"$fullText\"'); } - static Future tapParticipants(FlutterDriver driver) async { - await driver.tap(find.byValueKey('author-info')); + static Future tapParticipants(FlutterDriver? driver) async { + await driver?.tap(find.byValueKey('author-info')); } - static Future initiateEmailReplyAll(FlutterDriver driver) async { - await driver.tap(find.byType('FloatingActionButton')); - await driver.tap(find.text('Reply All')); + static Future initiateEmailReplyAll(FlutterDriver? driver) async { + await driver?.tap(find.byType('FloatingActionButton')); + await driver?.tap(find.text('Reply All')); } } diff --git a/apps/flutter_parent/test_driver/pages/conversation_list_page.dart b/apps/flutter_parent/test_driver/pages/conversation_list_page.dart index 58e6d1afc9..43d1aa38df 100644 --- a/apps/flutter_parent/test_driver/pages/conversation_list_page.dart +++ b/apps/flutter_parent/test_driver/pages/conversation_list_page.dart @@ -19,17 +19,17 @@ import 'package:test/test.dart'; class ConversationListPage { /// Since subjects/messages/contexts can be pretty complex, allow for portions of those /// fields to be verified. - static Future verifyConversationDataDisplayed(FlutterDriver driver, int index, - {List partialSubjects: null, - List partialBodies: null, - List partialContexts: null}) async { + static Future verifyConversationDataDisplayed(FlutterDriver? driver, int index, + {List? partialSubjects = null, + List? partialBodies = null, + List? partialContexts = null}) async { // Validate any specified partial subjects if (partialSubjects != null) { var finder = find.byValueKey('conversation_subject_$index'); - await driver.scrollIntoView(finder); - var fullText = await driver.getText(finder); + await driver?.scrollIntoView(finder); + var fullText = await driver?.getText(finder); for (String partialSubject in partialSubjects) { - expect(fullText.toLowerCase().contains(partialSubject.toLowerCase()), true, + expect(fullText?.toLowerCase().contains(partialSubject.toLowerCase()), true, reason: "Message subject \"$partialSubject\" in \"$fullText\""); } } @@ -37,10 +37,10 @@ class ConversationListPage { // Validate any specified partial contexts if (partialContexts != null) { var finder = find.byValueKey('conversation_context_$index'); - await driver.scrollIntoView(finder); - var fullText = await driver.getText(finder); + await driver?.scrollIntoView(finder); + var fullText = await driver?.getText(finder); for (String partialContext in partialContexts) { - expect(fullText.toLowerCase().contains(partialContext.toLowerCase()), true, + expect(fullText?.toLowerCase().contains(partialContext.toLowerCase()), true, reason: "Message context \"$partialContext\" in \"$fullText\""); } } @@ -48,25 +48,25 @@ class ConversationListPage { // Validate any specified partial messages bodies if (partialBodies != null) { var finder = find.byValueKey('conversation_message_$index'); - await driver.scrollIntoView(finder); - var fullText = await driver.getText(finder); + await driver?.scrollIntoView(finder); + var fullText = await driver?.getText(finder); for (String partialMessage in partialBodies) { - expect(fullText.toLowerCase().contains(partialMessage.toLowerCase()), true, + expect(fullText?.toLowerCase().contains(partialMessage.toLowerCase()), true, reason: "Message body \"$partialMessage\" in \"$fullText\""); } } } /// Gets you to the CreateConversationScreen - static Future initiateCreateEmail(FlutterDriver driver, Course forCourse) async { - await driver.tap(find.byType('FloatingActionButton')); - await driver.tap(find.byValueKey('course_list_course_${forCourse.id}')); + static Future initiateCreateEmail(FlutterDriver? driver, Course forCourse) async { + await driver?.tap(find.byType('FloatingActionButton')); + await driver?.tap(find.byValueKey('course_list_course_${forCourse.id}')); await Future.delayed(const Duration(seconds: 1)); // Allow time for population } - static Future selectMessage(FlutterDriver driver, int index) async { + static Future selectMessage(FlutterDriver? driver, int index) async { var finder = find.byValueKey('conversation_subject_$index'); - await driver.scrollIntoView(finder); - await driver.tap(finder); + await driver?.scrollIntoView(finder); + await driver?.tap(finder); } } diff --git a/apps/flutter_parent/test_driver/pages/course_details_page.dart b/apps/flutter_parent/test_driver/pages/course_details_page.dart index 9a4f400b78..577feb9028 100644 --- a/apps/flutter_parent/test_driver/pages/course_details_page.dart +++ b/apps/flutter_parent/test_driver/pages/course_details_page.dart @@ -17,19 +17,19 @@ import 'package:flutter_driver/flutter_driver.dart'; /// the course grades page or the syllabus page. (Grades/syllabus are /// nested in the course details page.) class CourseDetailsPage { - static Future selectSyllabus(FlutterDriver driver) async { - await driver.tap(find.text("SYLLABUS")); + static Future selectSyllabus(FlutterDriver? driver) async { + await driver?.tap(find.text("SYLLABUS")); } - static Future selectGrades(FlutterDriver driver) async { - await driver.tap(find.text("GRADES")); + static Future selectGrades(FlutterDriver? driver) async { + await driver?.tap(find.text("GRADES")); } - static Future selectSummary(FlutterDriver driver) async { - await driver.tap(find.text("SUMMARY")); + static Future selectSummary(FlutterDriver? driver) async { + await driver?.tap(find.text("SUMMARY")); } - static Future initiateCreateEmail(FlutterDriver driver) async { - await driver.tap(find.byType('FloatingActionButton')); + static Future initiateCreateEmail(FlutterDriver? driver) async { + await driver?.tap(find.byType('FloatingActionButton')); } } diff --git a/apps/flutter_parent/test_driver/pages/course_grades_page.dart b/apps/flutter_parent/test_driver/pages/course_grades_page.dart index 5034d24dc7..a44e2ae6ac 100644 --- a/apps/flutter_parent/test_driver/pages/course_grades_page.dart +++ b/apps/flutter_parent/test_driver/pages/course_grades_page.dart @@ -19,41 +19,41 @@ import 'package:test/test.dart'; import '../flutter_driver_extensions.dart'; class CourseGradesPage { - static Future verifyTotalGradeContains(FlutterDriver driver, String text) async { - var totalGradeText = await driver.getTextWithRefreshes(_totalGradeFinder); - expect(totalGradeText.toLowerCase().contains(text.toLowerCase()), true, + static Future verifyTotalGradeContains(FlutterDriver? driver, String text) async { + var totalGradeText = await driver?.getTextWithRefreshes(_totalGradeFinder); + expect(totalGradeText?.toLowerCase().contains(text.toLowerCase()), true, reason: "Expected total grade to contain $text"); } - static Future verifyAssignment(FlutterDriver driver, Assignment assignment, - {String grade = null, String status = null}) async { + static Future verifyAssignment(FlutterDriver? driver, Assignment assignment, + {String? grade = null, String? status = null}) async { var rowFinder = _assignmentRowFinder(assignment); - await driver.scrollIntoView(rowFinder); + await driver?.scrollIntoView(rowFinder); var nameFinder = _assignmentNameFinder(assignment); - var nameText = await driver.getTextWithRefreshes(nameFinder); + var nameText = await driver?.getTextWithRefreshes(nameFinder); expect(nameText, assignment.name, reason: "Expected assignment name of ${assignment.name}"); var gradeFinder = _assignmentGradeFinder(assignment); - var gradeText = await driver.getTextWithRefreshes(gradeFinder); - expect(gradeText.contains(assignment.pointsPossible.toInt().toString()), true, + var gradeText = await driver?.getTextWithRefreshes(gradeFinder); + expect(gradeText?.contains(assignment.pointsPossible.toInt().toString()), true, reason: "Expected grade to contain ${assignment.pointsPossible.toInt()}"); if (grade != null) { - expect(gradeText.contains(grade), true, reason: "Expected grade to contain $grade"); + expect(gradeText?.contains(grade), true, reason: "Expected grade to contain $grade"); } if (status != null) { var statusFinder = _assignmentStatusFinder(assignment); - var statusText = await driver.getTextWithRefreshes(statusFinder); - expect(statusText.toLowerCase().contains(status.toLowerCase()), true, + var statusText = await driver?.getTextWithRefreshes(statusFinder); + expect(statusText?.toLowerCase().contains(status.toLowerCase()), true, reason: "Expected status to contain $status"); } } - static Future selectAssignment(FlutterDriver driver, Assignment assignment) async { + static Future selectAssignment(FlutterDriver? driver, Assignment assignment) async { var rowFinder = _assignmentRowFinder(assignment); - await driver.scrollIntoView(rowFinder); - await driver.tap(_assignmentNameFinder(assignment)); + await driver?.scrollIntoView(rowFinder); + await driver?.tap(_assignmentNameFinder(assignment)); } static final _totalGradeFinder = find.byValueKey("total_grade"); @@ -74,7 +74,7 @@ class CourseGradesPage { return find.byValueKey("assignment_${assignment.id}_grade"); } - static Future initiateCreateEmail(FlutterDriver driver) async { - await driver.tap(find.byType('FloatingActionButton')); + static Future initiateCreateEmail(FlutterDriver? driver) async { + await driver?.tap(find.byType('FloatingActionButton')); } } diff --git a/apps/flutter_parent/test_driver/pages/course_summary_page.dart b/apps/flutter_parent/test_driver/pages/course_summary_page.dart index 8e7f352881..c8bda44eda 100644 --- a/apps/flutter_parent/test_driver/pages/course_summary_page.dart +++ b/apps/flutter_parent/test_driver/pages/course_summary_page.dart @@ -24,58 +24,58 @@ import 'assignment_details_page.dart'; import 'event_details_page.dart'; class CourseSummaryPage { - static Future verifyAssignmentPresent(FlutterDriver driver, Assignment assignment) async { + static Future verifyAssignmentPresent(FlutterDriver? driver, Assignment assignment) async { var titleFinder = find.byValueKey('summary_item_title_${assignment.id}'); - await driver.scrollIntoView(titleFinder); - var title = await driver.getText(titleFinder); + await driver?.scrollIntoView(titleFinder); + var title = await driver?.getText(titleFinder); expect(title, assignment.name, reason: "assignment title"); await _validateDueDate(driver, assignment.id, assignment.dueAt); // Lets click through to the assignment details, validate them, and come back - await driver.tap(titleFinder); + await driver?.tap(titleFinder); await AssignmentDetailsPage.validateUnsubmittedAssignment(driver, assignment); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); } - static Future verifyQuizPresent(FlutterDriver driver, Quiz quiz) async { + static Future verifyQuizPresent(FlutterDriver? driver, Quiz quiz) async { var titleFinder = find.byValueKey('summary_item_title_${quiz.id}'); - await driver.scrollIntoView(titleFinder); - var title = await driver.getText(titleFinder); + await driver?.scrollIntoView(titleFinder); + var title = await driver?.getText(titleFinder); expect(title, quiz.title, reason: "quiz title"); await _validateDueDate(driver, quiz.id, quiz.dueAt); // Lets click through to the quiz/assignment details, validate them, and come back - await driver.tap(titleFinder); + await driver?.tap(titleFinder); await AssignmentDetailsPage.validateUnsubmittedQuiz(driver, quiz); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); } - static Future verifyEventPresent(FlutterDriver driver, ScheduleItem event) async { + static Future verifyEventPresent(FlutterDriver? driver, ScheduleItem event) async { var titleFinder = find.byValueKey('summary_item_title_${event.id}'); - await driver.scrollIntoView(titleFinder); - var title = await driver.getText(titleFinder); + await driver?.scrollIntoView(titleFinder); + var title = await driver?.getText(titleFinder); expect(title, event.title, reason: "calendar event title"); await _validateDueDate(driver, event.id, event.isAllDay ? event.allDayDate : event.startAt); // Let's click through to the event details, validate them, and come back - await driver.tap(titleFinder); + await driver?.tap(titleFinder); await EventDetailsPage.verifyEventDisplayed(driver, event); - await driver.tap(find.pageBack()); + await driver?.tap(find.pageBack()); } - static Future _validateDueDate(FlutterDriver driver, String itemId, DateTime dueDate) async { + static Future _validateDueDate(FlutterDriver? driver, String itemId, DateTime? dueDate) async { var dateFinder = find.byValueKey('summary_item_subtitle_${itemId}'); - await driver.scrollIntoView(dateFinder); - var text = await driver.getText(dateFinder); + await driver?.scrollIntoView(dateFinder); + var text = await driver?.getText(dateFinder); if (dueDate == null) { expect(text, "No Due Date", reason: "Due date"); } else { var localDate = dueDate.toLocal(); String date = (DateFormat.MMMd(supportedDateLocale)).format(localDate); String time = (DateFormat.jm(supportedDateLocale)).format(localDate); - expect(text.contains(date), true, reason: "Expected due date ($text) to contain $date"); - expect(text.contains(time), true, reason: "Expected due date ($text) to contain $time"); + expect(text?.contains(date), true, reason: "Expected due date ($text) to contain $date"); + expect(text?.contains(time), true, reason: "Expected due date ($text) to contain $time"); } } } diff --git a/apps/flutter_parent/test_driver/pages/dashboard_page.dart b/apps/flutter_parent/test_driver/pages/dashboard_page.dart index c42edbd666..c9eeb41291 100644 --- a/apps/flutter_parent/test_driver/pages/dashboard_page.dart +++ b/apps/flutter_parent/test_driver/pages/dashboard_page.dart @@ -20,62 +20,62 @@ import 'package:test/test.dart'; import '..//flutter_driver_extensions.dart'; class DashboardPage { - static Future verifyCourse(FlutterDriver driver, Course course, {String grade = null}) async { - var actualName = await driver.getTextWithRefreshes(find.byValueKey("${course.courseCode}_name")); + static Future verifyCourse(FlutterDriver? driver, Course course, {String? grade = null}) async { + var actualName = await driver?.getTextWithRefreshes(find.byValueKey("${course.courseCode}_name")); expect(actualName, course.name); - var actualCode = await driver.getText(find.byValueKey("${course.courseCode}_code")); + var actualCode = await driver?.getText(find.byValueKey("${course.courseCode}_code")); expect(actualCode, course.courseCode); if (grade != null) { var actualGrade = - await driver.getTextWithRefreshes(find.byValueKey("${course.courseCode}_grade"), expectedText: grade); + await driver?.getTextWithRefreshes(find.byValueKey("${course.courseCode}_grade"), expectedText: grade); expect(actualGrade, grade); } } - static Future verifyCourses(FlutterDriver driver, List courses) async { - await courses.forEach((course) async { + static Future verifyCourses(FlutterDriver? driver, List courses) async { + courses.forEach((course) async { await verifyCourse(driver, course); }); } - static Future selectCourse(FlutterDriver driver, Course course) async { - await driver.tapWithRefreshes(find.text(course.name)); + static Future selectCourse(FlutterDriver? driver, Course course) async { + await driver?.tapWithRefreshes(find.text(course.name)); } - static Future waitForRender(FlutterDriver driver) async { + static Future waitForRender(FlutterDriver? driver) async { print("Waiting for DashboardScreen to appear"); - await driver.waitFor(find.byType("DashboardScreen"), - timeout: Duration(seconds: 10)); // It can take a while sometimes... + await driver?.waitFor(find.byType("DashboardScreen"), + timeout: Duration(seconds: 30)); // It can take a while sometimes... } - static Future verifyStudentDisplayed(FlutterDriver driver, SeededUser student) async { - await driver.waitFor(find.text(student.shortName)); + static Future verifyStudentDisplayed(FlutterDriver? driver, SeededUser student) async { + await driver?.waitFor(find.text(student.shortName)); } - static Future changeStudent(FlutterDriver driver, SeededUser newStudent) async { + static Future changeStudent(FlutterDriver? driver, SeededUser newStudent) async { // Open the student list expansion - await driver.tap(find.byValueKey('student_expansion_touch_target')); + await driver?.tap(find.byValueKey('student_expansion_touch_target')); // Select the new student - await driver.tap(find.byValueKey("${newStudent.shortName}_text")); + await driver?.tap(find.byValueKey("${newStudent.shortName}_text")); await Future.delayed(Duration(seconds: 1)); // Wait for animation to complete. } - static Future openNavDrawer(FlutterDriver driver) async { - await driver.tap(find.byValueKey("drawer_menu")); + static Future openNavDrawer(FlutterDriver? driver) async { + await driver?.tap(find.byValueKey("drawer_menu")); } - static Future openInbox(FlutterDriver driver) async { + static Future openInbox(FlutterDriver? driver) async { await openNavDrawer(driver); - await driver.tap(find.text("Inbox")); + await driver?.tap(find.text("Inbox")); } - static Future openManageStudents(FlutterDriver driver) async { + static Future openManageStudents(FlutterDriver? driver) async { await openNavDrawer(driver); - await driver.tap(find.text("Manage Students")); + await driver?.tap(find.text("Manage Students")); } - static Future goToCalendar(FlutterDriver driver) async { - await driver.tap(find.text("Calendar")); + static Future goToCalendar(FlutterDriver? driver) async { + await driver?.tap(find.text("Calendar")); } } diff --git a/apps/flutter_parent/test_driver/pages/event_details_page.dart b/apps/flutter_parent/test_driver/pages/event_details_page.dart index 8ace34fe4f..b0bd031d93 100644 --- a/apps/flutter_parent/test_driver/pages/event_details_page.dart +++ b/apps/flutter_parent/test_driver/pages/event_details_page.dart @@ -17,17 +17,17 @@ import 'package:flutter_parent/models/schedule_item.dart'; import 'package:test/test.dart'; class EventDetailsPage { - static Future verifyEventDisplayed(FlutterDriver driver, ScheduleItem event) async { - var titleText = await driver.getText(find.byValueKey('event_details_title')); + static Future verifyEventDisplayed(FlutterDriver? driver, ScheduleItem event) async { + var titleText = await driver?.getText(find.byValueKey('event_details_title')); expect(titleText, event.title, reason: 'Event title'); if (event.locationName != null) { - var locationText = await driver.getText(find.byValueKey('event_details_location_line1')); + var locationText = await driver?.getText(find.byValueKey('event_details_location_line1')); expect(locationText, event.locationName, reason: 'event location name'); } if (event.locationAddress != null) { - var locationAddressText = await driver.getText(find.byValueKey('event_details_location_line2')); + var locationAddressText = await driver?.getText(find.byValueKey('event_details_location_line2')); expect(locationAddressText, event.locationAddress, reason: 'event location address'); } } diff --git a/apps/flutter_parent/test_driver/pages/manage_students_page.dart b/apps/flutter_parent/test_driver/pages/manage_students_page.dart index 0df678f509..89433327ad 100644 --- a/apps/flutter_parent/test_driver/pages/manage_students_page.dart +++ b/apps/flutter_parent/test_driver/pages/manage_students_page.dart @@ -16,16 +16,16 @@ import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_parent/models/dataseeding/seeded_user.dart'; class ManageStudentsPage { - static Future addStudent(FlutterDriver driver, String pairingCode) async { - await driver.tap(find.byType("FloatingActionButton")); - await driver.tap(find.text("Pairing Code")); // Choose between pairing code and qr-code - await driver.tap(find.byType("TextFormField")); - await driver.enterText(pairingCode); - await driver.tap(find.text("OK")); + static Future addStudent(FlutterDriver? driver, String pairingCode) async { + await driver?.tap(find.byType("FloatingActionButton")); + await driver?.tap(find.text("Pairing Code")); // Choose between pairing code and qr-code + await driver?.tap(find.byType("TextFormField")); + await driver?.enterText(pairingCode); + await driver?.tap(find.text("OK")); } - static Future verifyStudentDisplayed(FlutterDriver driver, SeededUser user) async { - await driver.waitFor( + static Future verifyStudentDisplayed(FlutterDriver? driver, SeededUser user) async { + await driver?.waitFor( find.descendant(of: find.byValueKey('studentTextHero${user.id}'), matching: find.text(user.shortName))); } } diff --git a/apps/flutter_parent/test_driver/summary.dart b/apps/flutter_parent/test_driver/summary.dart index ae10fd4d39..612888dd85 100644 --- a/apps/flutter_parent/test_driver/summary.dart +++ b/apps/flutter_parent/test_driver/summary.dart @@ -36,13 +36,12 @@ void main() async { var course = data.courses[0]; var parent = data.parents[0]; - var assignment = - await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()); - var quiz = await QuizSeedApi.createQuiz(course.id, "EZ Quiz", DateTime.now().add(Duration(days: 1)).toUtc()); + var assignment = (await AssignmentSeedApi.createAssignment(course.id, dueAt: DateTime.now().add(Duration(days: 1)).toUtc()))!; + var quiz = (await QuizSeedApi.createQuiz(course.id, "EZ Quiz", DateTime.now().add(Duration(days: 1)).toUtc()))!; var now = DateTime.now(); - var calendarEvent = await CalendarSeedApi.createCalendarEvent( + var calendarEvent = (await CalendarSeedApi.createCalendarEvent( course.id, "Calendar Event", DateTime(now.year, now.month, now.day).toUtc(), - description: "Description", allDay: true, locationName: "Location Name", locationAddress: "Location Address"); + description: "Description", allDay: true, locationName: "Location Name", locationAddress: "Location Address"))!; // Sign in the parent await AppSeedUtils.signIn(parent); diff --git a/apps/flutter_parent/test_driver/summary_test.dart b/apps/flutter_parent/test_driver/summary_test.dart index 4f1af5da0e..ace824e555 100644 --- a/apps/flutter_parent/test_driver/summary_test.dart +++ b/apps/flutter_parent/test_driver/summary_test.dart @@ -28,7 +28,7 @@ import 'pages/course_summary_page.dart'; import 'pages/dashboard_page.dart'; void main() { - FlutterDriver driver; + FlutterDriver? driver; // Connect to the Flutter driver before running any tests. setUpAll(() async { @@ -38,7 +38,7 @@ void main() { // Close the connection to the driver after the tests have completed. tearDownAll(() async { if (driver != null) { - driver.close(); + driver?.close(); } }); @@ -47,14 +47,14 @@ void main() { // verifies that they show up correctly on the summary page. test('Summary E2E', () async { // Wait for seeding to complete - var seedContext = await DriverSeedUtils.waitForSeedingToComplete(driver); + var seedContext = (await DriverSeedUtils.waitForSeedingToComplete(driver))!; print("driver: Seeding complete!"); - var parent = seedContext.getNamedObject("parent"); - var course = seedContext.getNamedObject("course"); - var assignment = seedContext.getNamedObject("assignment"); - var quiz = seedContext.getNamedObject("quiz"); - var event = seedContext.getNamedObject("event"); + var parent = seedContext.getNamedObject("parent")!; + var course = seedContext.getNamedObject("course")!; + var assignment = seedContext.getNamedObject("assignment")!; + var quiz = seedContext.getNamedObject("quiz")!; + var event = seedContext.getNamedObject("event")!; // Let's check that all of our assignments, quizzes and announcements are displayed await DashboardPage.waitForRender(driver); diff --git a/apps/student/build.gradle b/apps/student/build.gradle index bc4c0c3bec..eb671160b8 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -50,8 +50,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 254 - versionName = '6.26.0' + versionCode = 255 + versionName = '6.26.1' vectorDrawables.useSupportLibrary = true multiDexEnabled = true diff --git a/apps/student/flank.yml b/apps/student/flank.yml index 7f740f9b62..e6a449008d 100644 --- a/apps/student/flank.yml +++ b/apps/student/flank.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e.yml b/apps/student/flank_e2e.yml index df540a43ad..3bf18a37ee 100644 --- a/apps/student/flank_e2e.yml +++ b/apps/student/flank_e2e.yml @@ -13,7 +13,7 @@ gcloud: timeout: 60m test-targets: - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_e2e_lowres.yml b/apps/student/flank_e2e_lowres.yml index d7862027cb..d828548431 100644 --- a/apps/student/flank_e2e_lowres.yml +++ b/apps/student/flank_e2e_lowres.yml @@ -13,10 +13,10 @@ gcloud: timeout: 60m test-targets: - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E device: - model: NexusLowRes - version: 26 + version: 29 locale: en_US orientation: portrait diff --git a/apps/student/flank_e2e_min.yml b/apps/student/flank_e2e_min.yml index f494132881..48817ea5d4 100644 --- a/apps/student/flank_e2e_min.yml +++ b/apps/student/flank_e2e_min.yml @@ -13,7 +13,7 @@ gcloud: timeout: 60m test-targets: - annotation com.instructure.canvas.espresso.E2E - - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E device: - model: Nexus6P version: 26 diff --git a/apps/student/flank_e2e_offline.yml b/apps/student/flank_e2e_offline.yml new file mode 100644 index 0000000000..4d76f14bd3 --- /dev/null +++ b/apps/student/flank_e2e_offline.yml @@ -0,0 +1,26 @@ +gcloud: + project: delta-essence-114723 +# Use the next two lines to run locally +# app: ./build/outputs/apk/qa/debug/student-qa-debug.apk +# test: ./build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + app: ./apps/student/build/outputs/apk/qa/debug/student-qa-debug.apk + test: ./apps/student/build/outputs/apk/androidTest/qa/debug/student-qa-debug-androidTest.apk + results-bucket: android-student + auto-google-login: true + use-orchestrator: true + performance-metrics: false + record-video: true + timeout: 60m + test-targets: + - annotation com.instructure.canvas.espresso.OfflineE2E + - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + device: + - model: Pixel2.arm + version: 29 + locale: en_US + orientation: portrait + +flank: + testShards: 1 + testRuns: 1 + diff --git a/apps/student/flank_landscape.yml b/apps/student/flank_landscape.yml index d3f2add67c..ebd8fc2896 100644 --- a/apps/student/flank_landscape.yml +++ b/apps/student/flank_landscape.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubLandscape, com.instructure.canvas.espresso.OfflineE2E device: - model: Pixel2.arm version: 29 diff --git a/apps/student/flank_multi_api_level.yml b/apps/student/flank_multi_api_level.yml index 4e49202e63..b0967a38ec 100644 --- a/apps/student/flank_multi_api_level.yml +++ b/apps/student/flank_multi_api_level.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubMultiAPILevel, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug, com.instructure.canvas.espresso.OfflineE2E device: - model: NexusLowRes version: 27 diff --git a/apps/student/flank_tablet.yml b/apps/student/flank_tablet.yml index 0635516ca8..ba6afecd57 100644 --- a/apps/student/flank_tablet.yml +++ b/apps/student/flank_tablet.yml @@ -12,7 +12,7 @@ gcloud: record-video: true timeout: 60m test-targets: - - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet + - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.StubTablet, com.instructure.canvas.espresso.OfflineE2E device: - model: MediumTablet.arm version: 29 diff --git a/apps/student/src/main/java/com/instructure/student/holders/ModuleEmptyViewHolder.kt b/apps/student/src/androidTest/java/com/instructure/student/espresso/fakes/FakeNetworkStateProvider.kt similarity index 53% rename from apps/student/src/main/java/com/instructure/student/holders/ModuleEmptyViewHolder.kt rename to apps/student/src/androidTest/java/com/instructure/student/espresso/fakes/FakeNetworkStateProvider.kt index 924833288b..f77953ad08 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/ModuleEmptyViewHolder.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/espresso/fakes/FakeNetworkStateProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 - present Instructure, Inc. + * Copyright (C) 2023 - present Instructure, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,19 +14,19 @@ * along with this program. If not, see . * */ -package com.instructure.student.holders -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.instructure.student.R -import com.instructure.student.databinding.ViewholderModuleEmptyBinding +package com.instructure.student.espresso.fakes -class ModuleEmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun bind(text: String?) = with(ViewholderModuleEmptyBinding.bind(itemView)){ - titleText.text = text - } +import androidx.lifecycle.LiveData +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.pandautils.utils.orDefault + +class FakeNetworkStateProvider(private val fakeLiveData: LiveData) : NetworkStateProvider { + + override val isOnlineLiveData: LiveData + get() = fakeLiveData - companion object { - const val HOLDER_RES_ID = R.layout.viewholder_module_empty + override fun isOnline(): Boolean { + return fakeLiveData.value.orDefault() } -} +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt index 65b44544cd..755499d291 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AnnouncementsE2ETest.kt @@ -63,13 +63,13 @@ class AnnouncementsE2ETest : StudentTest() { courseBrowserPage.selectAnnouncements() Log.d(STEP_TAG,"Assert that ${announcement.title} announcement is displayed.") - discussionListPage.assertTopicDisplayed(announcement.title) + announcementListPage.assertTopicDisplayed(announcement.title) Log.d(STEP_TAG, "Assert that ${lockedAnnouncement.title} announcement is really locked so that the 'locked' icon is displayed.") - discussionListPage.assertAnnouncementLocked(lockedAnnouncement.title) + announcementListPage.assertAnnouncementLocked(lockedAnnouncement.title) Log.d(STEP_TAG, "Select ${lockedAnnouncement.title} announcement and assert if we are landing on the Discussion Details Page.") - discussionListPage.selectTopic(lockedAnnouncement.title) + announcementListPage.selectTopic(lockedAnnouncement.title) discussionDetailsPage.assertTitleText(lockedAnnouncement.title) Log.d(STEP_TAG, "Assert that the 'Reply' button is not available on a locked announcement. Navigate back to Announcement List Page.") @@ -77,7 +77,7 @@ class AnnouncementsE2ETest : StudentTest() { Espresso.pressBack() Log.d(STEP_TAG,"Select ${announcement.title} announcement and assert if we are landing on the Discussion Details Page.") - discussionListPage.selectTopic(announcement.title) + announcementListPage.selectTopic(announcement.title) discussionDetailsPage.assertTitleText(announcement.title) val replyMessage = "Reply text" @@ -92,36 +92,36 @@ class AnnouncementsE2ETest : StudentTest() { Log.d(STEP_TAG,"Click on Search button and type ${announcement.title} to the search input field.") Espresso.pressBack() - discussionListPage.searchable.clickOnSearchButton() - discussionListPage.searchable.typeToSearchBar(announcement.title) + announcementListPage.searchable.clickOnSearchButton() + announcementListPage.searchable.typeToSearchBar(announcement.title) Log.d(STEP_TAG,"Assert that only the matching announcement is displayed on the Discussion List Page.") - discussionListPage.pullToUpdate() - discussionListPage.assertTopicDisplayed(announcement.title) - discussionListPage.assertTopicNotDisplayed(lockedAnnouncement.title) + announcementListPage.pullToUpdate() + announcementListPage.assertTopicDisplayed(announcement.title) + announcementListPage.assertTopicNotDisplayed(lockedAnnouncement.title) Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Discussion List Page.") - discussionListPage.searchable.clickOnClearSearchButton() - discussionListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title) - discussionListPage.assertTopicDisplayed(announcement.title) + announcementListPage.searchable.clickOnClearSearchButton() + announcementListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title) + announcementListPage.assertTopicDisplayed(announcement.title) Log.d(STEP_TAG,"Type a search value to the search input field which does not much with any of the existing announcements.") - discussionListPage.searchable.typeToSearchBar("Non existing announcement title") + announcementListPage.searchable.typeToSearchBar("Non existing announcement title") sleep(3000) //We need this wait here to let make sure the search process has finished. Log.d(STEP_TAG,"Assert that the empty view is displayed and none of the announcements are appearing on the page.") - discussionListPage.assertEmpty() - discussionListPage.assertTopicNotDisplayed(announcement.title) - discussionListPage.assertTopicNotDisplayed(lockedAnnouncement.title) + announcementListPage.assertEmpty() + announcementListPage.assertTopicNotDisplayed(announcement.title) + announcementListPage.assertTopicNotDisplayed(lockedAnnouncement.title) Log.d(STEP_TAG,"Clear search input field value and assert if all the announcements are displayed again on the Discussion List Page.") - discussionListPage.searchable.clickOnClearSearchButton() - discussionListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title) - discussionListPage.assertTopicDisplayed(announcement.title) + announcementListPage.searchable.clickOnClearSearchButton() + announcementListPage.waitForDiscussionTopicToDisplay(lockedAnnouncement.title) + announcementListPage.assertTopicDisplayed(announcement.title) Log.d(STEP_TAG,"Refresh the page and assert that after refresh, still all the announcements are displayed.") refresh() - discussionListPage.assertTopicDisplayed(announcement.title) - discussionListPage.assertTopicDisplayed(lockedAnnouncement.title) + announcementListPage.assertTopicDisplayed(announcement.title) + announcementListPage.assertTopicDisplayed(lockedAnnouncement.title) } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt index 018271a8d4..f546bc83ea 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt @@ -19,10 +19,12 @@ package com.instructure.student.ui.e2e import android.os.SystemClock.sleep import android.util.Log import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.rule.GrantPermissionRule import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentGroupsApi import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.CoursesApi import com.instructure.dataseeding.api.SubmissionsApi import com.instructure.dataseeding.model.AssignmentApiModel import com.instructure.dataseeding.model.AttachmentApiModel @@ -42,6 +44,7 @@ import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.student.ui.pages.AssignmentListPage import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.ViewUtils import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin import com.instructure.student.ui.utils.uploadTextFile @@ -657,7 +660,268 @@ class AssignmentsE2ETest: StudentTest() { submissionDetailsPage.assertTextSubmissionDisplayedAsComment() } - private fun createAssignment( + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.E2E) + fun showOnlyLetterGradeOnDashboardAndAssignmentListPageE2E() { + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") + val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(PREPARATION_TAG,"Grade submission: ${pointsTextAssignment.name} with 12 points.") + gradeSubmission(teacher, course, pointsTextAssignment.id, student, "12") + + Log.d(STEP_TAG, "Refresh the Dashboard page. Assert that the course grade is 80%.") + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "80%") + + Log.d(PREPARATION_TAG, "Update ${course.name} course's settings: Enable restriction for quantitative data.") + var restrictQuantitativeDataMap = mutableMapOf() + restrictQuantitativeDataMap["restrict_quantitative_data"] = true + CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) + + Log.d(STEP_TAG, "Refresh the Dashboard page. Assert that the course grade is B-, as it is converted to letter grade because of the restriction.") + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "B-") + + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") + val percentageAssignment = createAssignment(course.id, teacher, GradingType.PERCENT, 15.0, 1.days.fromNow.iso8601) + + Log.d(PREPARATION_TAG,"Grade submission: ${percentageAssignment.name} with 66% of the maximum points (aka. 10).") + gradeSubmission(teacher, course, percentageAssignment.id, student, "10") + + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") + val letterGradeAssignment = createAssignment(course.id, teacher, GradingType.LETTER_GRADE, 15.0, 1.days.fromNow.iso8601) + + Log.d(PREPARATION_TAG,"Grade submission: ${letterGradeAssignment.name} with C.") + gradeSubmission(teacher, course, letterGradeAssignment.id, student, "C") + + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") + val passFailAssignment = createAssignment(course.id, teacher, GradingType.PASS_FAIL, 15.0, 1.days.fromNow.iso8601) + + Log.d(PREPARATION_TAG,"Grade submission: ${passFailAssignment.name} with 'Incomplete'.") + gradeSubmission(teacher, course, passFailAssignment.id, student, "Incomplete") + + Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") + val gpaScaleAssignment = createAssignment(course.id, teacher, GradingType.GPA_SCALE, 15.0, 1.days.fromNow.iso8601) + + Log.d(PREPARATION_TAG,"Grade submission: ${gpaScaleAssignment.name} with 3.7.") + gradeSubmission(teacher, course, gpaScaleAssignment.id, student, "3.7") + + Log.d(STEP_TAG, "Refresh the Dashboard page to let the newly added submissions and their grades propagate.") + dashboardPage.refresh() + + Log.d(STEP_TAG,"Select course: ${course.name}.") + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG,"Navigate to course Assignments Page.") + courseBrowserPage.selectAssignments() + + Log.d(STEP_TAG, "Assert that all the different types of assignments' grades has been converted properly.") + assignmentListPage.assertAssignmentDisplayedWithGrade(pointsTextAssignment.name, "B-") + assignmentListPage.assertAssignmentDisplayedWithGrade(percentageAssignment.name, "D") + assignmentListPage.assertAssignmentDisplayedWithGrade(letterGradeAssignment.name, "C") + assignmentListPage.assertAssignmentDisplayedWithGrade(passFailAssignment.name, "Incomplete") + assignmentListPage.assertAssignmentDisplayedWithGrade(gpaScaleAssignment.name, "F") + + Log.d(STEP_TAG, "Click on '${pointsTextAssignment.name}' assignment and assert that the corresponding letter grade is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(pointsTextAssignment) + assignmentDetailsPage.assertGradeDisplayed("B-") + assignmentDetailsPage.assertScoreNotDisplayed() + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${percentageAssignment.name}' assignment and assert that the corresponding letter grade is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(percentageAssignment) + assignmentDetailsPage.assertGradeDisplayed("D") + assignmentDetailsPage.assertScoreNotDisplayed() + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${letterGradeAssignment.name}' assignment and assert that the corresponding letter grade is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(letterGradeAssignment) + assignmentDetailsPage.assertGradeDisplayed("C") + assignmentDetailsPage.assertScoreNotDisplayed() + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${passFailAssignment.name}' assignment and assert that the corresponding letter grade is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(passFailAssignment) + assignmentDetailsPage.assertGradeDisplayed("Incomplete") + assignmentDetailsPage.assertScoreNotDisplayed() + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${gpaScaleAssignment.name}' assignment and assert that the corresponding letter grade is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(gpaScaleAssignment) + assignmentDetailsPage.assertGradeDisplayed("F") + assignmentDetailsPage.assertScoreNotDisplayed() + Espresso.pressBack() + + Log.d(PREPARATION_TAG, "Update ${course.name} course's settings: Disable restriction for quantitative data.") + restrictQuantitativeDataMap["restrict_quantitative_data"] = false + CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) + + Log.d(STEP_TAG, "Refresh the Assignment List Page. Assert that all the different types of assignments' grades" + + "has been shown as their original grade types, since the restriction has been turned off.") + assignmentListPage.refresh() + assignmentListPage.assertAssignmentDisplayedWithGrade(pointsTextAssignment.name, "12/15") + assignmentListPage.assertAssignmentDisplayedWithGrade(percentageAssignment.name, "66.67%") + assignmentListPage.assertAssignmentDisplayedWithGrade(letterGradeAssignment.name, "11.4/15 (C)") + assignmentListPage.assertAssignmentDisplayedWithGrade(passFailAssignment.name, "Incomplete") + assignmentListPage.assertAssignmentDisplayedWithGrade(gpaScaleAssignment.name, "3.7/15 (F)") + + Log.d(STEP_TAG, "Click on '${pointsTextAssignment.name}' assignment and assert that the corresponding grade and score is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(pointsTextAssignment) + assignmentDetailsPage.assertScoreDisplayed("12") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${percentageAssignment.name}' assignment and assert that the corresponding grade and score is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(percentageAssignment) + assignmentDetailsPage.assertScoreDisplayed("10") + assignmentDetailsPage.assertGradeDisplayed("66.67%") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${letterGradeAssignment.name}' assignment and assert that the corresponding grade and score is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(letterGradeAssignment) + assignmentDetailsPage.assertScoreDisplayed("11.4") + assignmentDetailsPage.assertGradeDisplayed("C") + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${passFailAssignment.name}' assignment and assert that the corresponding grade is displayed on it's details page, and no score displayed since it's a pass/fail assignment. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(passFailAssignment) + assignmentDetailsPage.assertGradeDisplayed("Incomplete") + assignmentDetailsPage.assertScoreNotDisplayed() + Espresso.pressBack() + + Log.d(STEP_TAG, "Click on '${gpaScaleAssignment.name}' assignment and assert that the corresponding grade and score is displayed on it's details page. Navigate back to Assignment List Page.") + assignmentListPage.clickAssignment(gpaScaleAssignment) + assignmentDetailsPage.assertScoreDisplayed("3.7") + assignmentDetailsPage.assertGradeDisplayed("F") + Espresso.pressBack() + + Log.d(STEP_TAG, "Navigate back to Dashboard Page.") + ViewUtils.pressBackButton(2) + + Log.d(STEP_TAG, "Assert that the course grade is F, as it is converted to letter grade because the disability of the restriction has not propagated yet.") + dashboardPage.assertCourseGrade(course.name, "F") + + Log.d(STEP_TAG, "Refresh the Dashboard page (to allow the disabled restriction to propagate). Assert that the course grade is 49.47%, since we can now show percentage and numeric data.") + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "49.47%") + } + + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.E2E) + fun showOnlyLetterGradeOnGradesPageE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.") + val pointsTextAssignment = createAssignment(course.id, teacher, GradingType.POINTS, 15.0, 1.days.fromNow.iso8601) + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(PREPARATION_TAG, "Grade submission: ${pointsTextAssignment.name} with 12 points.") + gradeSubmission(teacher, course, pointsTextAssignment.id, student, "12") + + Log.d(PREPARATION_TAG, "Update ${course.name} course's settings: Enable restriction for quantitative data.") + var restrictQuantitativeDataMap = mutableMapOf() + restrictQuantitativeDataMap["restrict_quantitative_data"] = true + CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) + + Log.d(STEP_TAG, "Refresh the Dashboard page. Assert that the course grade is B-, as it is converted to letter grade because of the restriction.") + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "B-") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.") + val percentageAssignment = createAssignment(course.id, teacher, GradingType.PERCENT, 15.0, 1.days.fromNow.iso8601) + + Log.d(PREPARATION_TAG, "Grade submission: ${percentageAssignment.name} with 66% of the maximum points (aka. 10).") + gradeSubmission(teacher, course, percentageAssignment.id, student, "10") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.") + val letterGradeAssignment = createAssignment( + course.id, + teacher, + GradingType.LETTER_GRADE, + 15.0, + 1.days.fromNow.iso8601 + ) + + Log.d(PREPARATION_TAG, "Grade submission: ${letterGradeAssignment.name} with C.") + gradeSubmission(teacher, course, letterGradeAssignment.id, student, "C") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.") + val passFailAssignment = createAssignment( + course.id, + teacher, + GradingType.PASS_FAIL, + 15.0, + 1.days.fromNow.iso8601 + ) + + Log.d(PREPARATION_TAG, "Grade submission: ${passFailAssignment.name} with 'Incomplete'.") + gradeSubmission(teacher, course, passFailAssignment.id, student, "Incomplete") + + Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for ${course.name} course.") + val gpaScaleAssignment = createAssignment( + course.id, + teacher, + GradingType.GPA_SCALE, + 15.0, + 1.days.fromNow.iso8601 + ) + + Log.d(PREPARATION_TAG, "Grade submission: ${gpaScaleAssignment.name} with 3.7.") + gradeSubmission(teacher, course, gpaScaleAssignment.id, student, "3.7") + + Log.d(STEP_TAG, "Refresh the Dashboard page to let the newly added submissions and their grades propagate.") + dashboardPage.refresh() + + Log.d(STEP_TAG, "Select course: ${course.name}. Select 'Grades' menu.") + dashboardPage.selectCourse(course) + courseBrowserPage.selectGrades() + + Log.d(STEP_TAG, "Assert that the Total Grade is F and all of the assignment grades are displayed properly (so they have been converted to letter grade).") + courseGradesPage.assertTotalGrade(ViewMatchers.withText("F")) + courseGradesPage.assertAssignmentDisplayed(pointsTextAssignment.name, "B-") + courseGradesPage.assertAssignmentDisplayed(percentageAssignment.name, "D") + courseGradesPage.assertAssignmentDisplayed(letterGradeAssignment.name, "C") + courseGradesPage.assertAssignmentDisplayed(passFailAssignment.name, "Incomplete") + if(isLowResDevice()) courseGradesPage.swipeUp() + courseGradesPage.assertAssignmentDisplayed(gpaScaleAssignment.name, "F") + + Log.d(PREPARATION_TAG, "Update ${course.name} course's settings: Enable restriction for quantitative data.") + restrictQuantitativeDataMap["restrict_quantitative_data"] = false + CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) + + Log.d(STEP_TAG, "Refresh the Course Grade Page.") + courseGradesPage.refresh() //First go to the top of the recycler view + courseGradesPage.refresh() //Actual refresh + + Log.d(STEP_TAG, "Assert that the Total Grade is 49.47% and all of the assignment grades are displayed properly. We now show numeric grades because restriction to quantitative data has been disabled.") + courseGradesPage.assertTotalGrade(ViewMatchers.withText("49.47%")) + courseGradesPage.assertAssignmentDisplayed(pointsTextAssignment.name, "12/15") + courseGradesPage.assertAssignmentDisplayed(percentageAssignment.name, "66.67%") + courseGradesPage.assertAssignmentDisplayed(letterGradeAssignment.name, "11.4/15 (C)") + courseGradesPage.assertAssignmentDisplayed(passFailAssignment.name, "Incomplete") + courseGradesPage.swipeUp() + courseGradesPage.assertAssignmentDisplayed(gpaScaleAssignment.name, "3.7/15 (F)") + } + + private fun createAssignment( courseId: Long, teacher: CanvasUserApiModel, gradingType: GradingType, diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt index c34c0326d6..e0fe3bcffa 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/DashboardE2ETest.kt @@ -88,7 +88,7 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertDisplaysCourse(course) } - Log.d(STEP_TAG, "Switch to back to Card View.") + Log.d(STEP_TAG, "Switch to back to Grid View.") dashboardPage.switchCourseView() for(course in data.coursesList) { @@ -100,7 +100,7 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertDisplaysGroup(group, course1) dashboardPage.assertDisplaysGroup(group2, course1) - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button. Assert that the Edit Dashboard Page is loaded.") + Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() @@ -122,7 +122,7 @@ class DashboardE2ETest : StudentTest() { courseBrowserPage.assertPageObjects() Espresso.pressBack() - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button. Assert that the Edit Dashboard Page is loaded.") + Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") dashboardPage.assertPageObjects() dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() @@ -185,7 +185,7 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertCourseGrade(course1.name, "N/A") dashboardPage.assertCourseGrade(course2.name, "N/A") - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button.") + Log.d(STEP_TAG,"Click on 'All Courses' button.") dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() @@ -202,7 +202,7 @@ class DashboardE2ETest : StudentTest() { dashboardPage.assertDisplaysGroup(group, course1) dashboardPage.assertGroupNotDisplayed(group2) - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button.") + Log.d(STEP_TAG,"Click on 'All Courses' button.") dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() Thread.sleep(2000) //It can be flaky without this 2 seconds @@ -220,7 +220,7 @@ class DashboardE2ETest : StudentTest() { Espresso.pressBack() Log.d(STEP_TAG,"Assert that both of the groups, '${group.name}' and '${group2.name}' are displayed" + - "since if there is no group selected on the Edit Dashboard page, we are showing all of them (this is the same logics as with the courses).") + "since if there is no group selected on the All Courses page, we are showing all of them (this is the same logics as with the courses).") dashboardPage.assertDisplaysGroup(group, course1) dashboardPage.assertDisplaysGroup(group2, course1) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index 6e8c9a8398..b2d370238f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -44,8 +44,7 @@ class InboxE2ETest: StudentTest() { @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) - fun testInboxE2E() { - + fun testInboxSelectedButtonActionsE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 2, teachers = 1, courses = 1) val teacher = data.teachersList[0] @@ -76,63 +75,6 @@ class InboxE2ETest: StudentTest() { inboxPage.assertHasConversation() inboxPage.assertConversationDisplayed(seededConversation) - Log.d(STEP_TAG,"Click on 'New Message' button.") - inboxPage.pressNewMessageButton() - - val newMessageSubject = "Hey There" - val newMessage = "Just checking in" - Log.d(STEP_TAG,"Create a new message with subject: $newMessageSubject, and message: $newMessage") - newMessagePage.populateMessage(course, student2, newMessageSubject, newMessage) - - Log.d(STEP_TAG,"Click on 'Send' button.") - newMessagePage.clickSend() - - Log.d(STEP_TAG,"Click on 'New Message' button.") - inboxPage.pressNewMessageButton() - - val newGroupMessageSubject = "Group Message" - val newGroupMessage = "Testing Group ${group.name}" - Log.d(STEP_TAG,"Create a new message with subject: $newGroupMessageSubject, and message: $newGroupMessage") - newMessagePage.populateGroupMessage(group, student2, newGroupMessageSubject, newGroupMessage) - - Log.d(STEP_TAG,"Click on 'Send' button.") - newMessagePage.clickSend() - - sleep(2000) // Allow time for messages to propagate - - Log.d(STEP_TAG,"Navigate back to Dashboard Page.") - inboxPage.goToDashboard() - dashboardPage.waitForRender() - - Log.d(STEP_TAG,"Log out with ${student1.name} student.") - leftSideNavigationDrawerPage.logout() - - Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId}.") - tokenLogin(student2) - dashboardPage.waitForRender() - - Log.d(STEP_TAG,"Open Inbox Page. Assert that both, the previously seeded 'normal' conversation and the group conversation are displayed.") - dashboardPage.clickInboxTab() - inboxPage.assertConversationDisplayed(seededConversation) - inboxPage.assertConversationDisplayed(newMessageSubject) - inboxPage.assertConversationDisplayed("Group Message") - - Log.d(STEP_TAG,"Select $newGroupMessageSubject conversation.") - inboxPage.openConversation(newMessageSubject) - val newReplyMessage = "This is a quite new reply message." - Log.d(STEP_TAG,"Reply to $newGroupMessageSubject conversation with '$newReplyMessage' message. Assert that the reply is displayed.") - inboxConversationPage.replyToMessage(newReplyMessage) - - Log.d(STEP_TAG,"Delete $newReplyMessage reply and assert is has been deleted.") - inboxConversationPage.deleteMessage(newReplyMessage) - inboxConversationPage.assertMessageNotDisplayed(newReplyMessage) - - Log.d(STEP_TAG,"Delete the whole '$newGroupMessageSubject' subject and assert that it has been removed from the conversation list on the Inbox Page.") - inboxConversationPage.deleteConversation() //After deletion we will be navigated back to Inbox Page - inboxPage.assertConversationNotDisplayed(newMessageSubject) - inboxPage.assertConversationDisplayed(seededConversation) - inboxPage.assertConversationDisplayed("Group Message") - Log.d(STEP_TAG,"Select ${seededConversation.subject} conversation. Assert that is has not been starred already.") inboxPage.openConversation(seededConversation) inboxConversationPage.assertNotStarred() @@ -163,9 +105,8 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Select 'Archived' conversation filter.") inboxPage.filterInbox("Archived") - Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation is displayed by the 'Archived' filter, and other conversations are not displayed.") + Log.d(STEP_TAG,"Assert that ${seededConversation.subject} conversation is displayed by the 'Archived' filter.") inboxPage.assertConversationDisplayed(seededConversation) - inboxPage.assertConversationNotDisplayed("Group Message") Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation. Assert that the selected number of conversations on the toolbar is 1." + "Unarchive it, and assert that it is not displayed in the 'ARCHIVED' scope any more.") @@ -181,79 +122,202 @@ class InboxE2ETest: StudentTest() { inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seededConversation.subject) - Log.d(STEP_TAG, "Select both of the conversations (${seededConversation.subject} and $newGroupMessageSubject) and star them." + - "Assert that the selected number of conversations on the toolbar is 2 and both of the has been starred.") - inboxPage.selectConversations(listOf(seededConversation.subject, newGroupMessageSubject)) - inboxPage.clickStar() - inboxPage.assertConversationStarred(seededConversation.subject) - inboxPage.assertConversationStarred(newGroupMessageSubject) - - Log.d(STEP_TAG, "Mark them as read (since if at least there is one unread selected, we are showing the 'Mark as Read' icon). Assert that both of them are read.") - inboxPage.clickMarkAsRead() - inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) - inboxPage.assertUnreadMarkerVisibility(newGroupMessageSubject, ViewMatchers.Visibility.GONE) - - Log.d(STEP_TAG, "Mark them as unread. Assert that both of them will became unread.") - inboxPage.clickMarkAsUnread() - inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) - inboxPage.assertUnreadMarkerVisibility(newGroupMessageSubject, ViewMatchers.Visibility.VISIBLE) + Log.d(STEP_TAG, "Select the conversations (${seededConversation.subject} and star it." + + "Assert that the selected number of conversations on the toolbar is 1 and the conversation is starred.") + inboxPage.selectConversations(listOf(seededConversation.subject)) + inboxPage.assertSelectedConversationNumber("1") + inboxPage.clickUnstar() + inboxPage.assertConversationNotStarred(seededConversation.subject) - Log.d(STEP_TAG, "Archive both of them. Assert that non of them are displayed in the 'INBOX' scope.") + Log.d(STEP_TAG, "Select the conversations (${seededConversation.subject} and archive it. Assert that it has not displayed in the 'INBOX' scope.") + inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.clickArchive() inboxPage.assertConversationNotDisplayed(seededConversation.subject) - inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) sleep(2000) - Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that both of the conversations are displayed there.") + Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that the conversation is displayed there.") inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seededConversation.subject) - inboxPage.assertConversationDisplayed(newGroupMessageSubject) - Log.d(STEP_TAG, "Navigate to 'UNREAD' scope and assert that none of the conversations are displayed there, because a conversation cannot be archived and unread at the same time.") + Log.d(STEP_TAG, "Navigate to 'UNREAD' scope and assert that the conversation is displayed there, because a conversation cannot be archived and unread at the same time.") inboxPage.filterInbox("Unread") inboxPage.assertConversationNotDisplayed(seededConversation.subject) - inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) - Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that both of the conversations are displayed there.") + Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that the conversation is NOT displayed there.") inboxPage.filterInbox("Starred") - inboxPage.assertConversationDisplayed(seededConversation.subject) - inboxPage.assertConversationDisplayed(newGroupMessageSubject) - - Log.d(STEP_TAG, "Select both of the conversations. Unstar them, and assert that none of them are displayed in the 'STARRED' scope.") - inboxPage.selectConversations(listOf(seededConversation.subject, newGroupMessageSubject)) - inboxPage.clickUnstar() inboxPage.assertConversationNotDisplayed(seededConversation.subject) - inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) - sleep(2000) + Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that ${seededConversation.subject} conversation is NOT displayed because it is archived yet.") + inboxPage.filterInbox("Inbox") + inboxPage.assertConversationNotDisplayed(seededConversation.subject) - Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that both of the conversations are displayed there.") + Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and Select the conversation. Star it, and assert that it has displayed in the 'STARRED' scope.") inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(seededConversation.subject) - inboxPage.assertConversationDisplayed(newGroupMessageSubject) + inboxPage.selectConversations(listOf(seededConversation.subject)) + inboxPage.clickStar() + inboxPage.assertConversationStarred(seededConversation.subject) - Log.d(STEP_TAG, "Select both of the conversations. Unarchive them, and assert that none of them are displayed in the 'ARCHIVED' scope.") - inboxPage.selectConversations(listOf(seededConversation.subject, newGroupMessageSubject)) + Log.d(STEP_TAG, "Select the conversation. Unarchive it, and assert that it has not displayed in the 'ARCHIVED' scope.") + inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.clickUnArchive() inboxPage.assertConversationNotDisplayed(seededConversation.subject) - inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) + + Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that the conversations is displayed there.") + inboxPage.filterInbox("Starred") + inboxPage.assertConversationDisplayed(seededConversation.subject) sleep(2000) - Log.d(STEP_TAG, "Navigate to 'INBOX' scope and assert that both of the conversations are displayed there.") + Log.d(STEP_TAG, "Navigate to 'INBOX' scope and assert that the conversation is displayed there because it is not archived yet.") inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seededConversation.subject) - inboxPage.assertConversationDisplayed(newGroupMessageSubject) + } + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) + fun testInboxMessageComposeReplyAndOptionMenuActionsE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 2, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student1 = data.studentsList[0] + val student2 = data.studentsList[1] + + val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) + val group = GroupsApi.createGroup(groupCategory.id, teacher.token) + Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.") + GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) + GroupsApi.createGroupMembership(group.id, student2.id, teacher.token) + + Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId}.") + tokenLogin(student1) + dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course) + + Log.d(STEP_TAG,"Open Inbox Page. Assert that the previously seeded conversation is displayed.") + dashboardPage.clickInboxTab() + inboxPage.assertInboxEmpty() + + Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") + val seededConversation = createConversation(teacher, student1, student2)[0] + + Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") + refresh() + inboxPage.assertHasConversation() + inboxPage.assertConversationDisplayed(seededConversation) + + Log.d(STEP_TAG,"Click on 'New Message' button.") + inboxPage.pressNewMessageButton() + + val newMessageSubject = "Hey There" + val newMessage = "Just checking in" + Log.d(STEP_TAG,"Create a new message with subject: $newMessageSubject, and message: $newMessage") + newMessagePage.populateMessage(course, student2, newMessageSubject, newMessage) + + Log.d(STEP_TAG,"Click on 'Send' button.") + newMessagePage.clickSend() + + Log.d(STEP_TAG,"Click on 'New Message' button.") + inboxPage.pressNewMessageButton() + + val newGroupMessageSubject = "Group Message" + val newGroupMessage = "Testing Group ${group.name}" + Log.d(STEP_TAG,"Create a new message with subject: $newGroupMessageSubject, and message: $newGroupMessage") + newMessagePage.populateGroupMessage(group, student2, newGroupMessageSubject, newGroupMessage) + + Log.d(STEP_TAG,"Click on 'Send' button.") + newMessagePage.clickSend() + + sleep(2000) // Allow time for messages to propagate + + Log.d(STEP_TAG,"Navigate back to Dashboard Page.") + inboxPage.goToDashboard() + dashboardPage.waitForRender() + + Log.d(STEP_TAG,"Log out with ${student1.name} student.") + leftSideNavigationDrawerPage.logout() + + Log.d(STEP_TAG,"Login with user: ${student2.name}, login id: ${student2.loginId}.") + tokenLogin(student2) + dashboardPage.waitForRender() + + Log.d(STEP_TAG,"Open Inbox Page. Assert that both, the previously seeded 'normal' conversation and the group conversation are displayed.") + dashboardPage.clickInboxTab() + inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(newMessageSubject) + inboxPage.assertConversationDisplayed("Group Message") + + Log.d(STEP_TAG,"Select $newGroupMessageSubject conversation.") + inboxPage.openConversation(newMessageSubject) + val newReplyMessage = "This is a quite new reply message." + Log.d(STEP_TAG,"Reply to $newGroupMessageSubject conversation with '$newReplyMessage' message. Assert that the reply is displayed.") + inboxConversationPage.replyToMessage(newReplyMessage) + + Log.d(STEP_TAG,"Delete $newReplyMessage reply and assert is has been deleted.") + inboxConversationPage.deleteMessage(newReplyMessage) + inboxConversationPage.assertMessageNotDisplayed(newReplyMessage) + + Log.d(STEP_TAG,"Delete the whole '$newGroupMessageSubject' subject and assert that it has been removed from the conversation list on the Inbox Page.") + inboxConversationPage.deleteConversation() //After deletion we will be navigated back to Inbox Page + inboxPage.assertConversationNotDisplayed(newMessageSubject) + inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed("Group Message") + + Log.d(STEP_TAG, "Navigate to 'INBOX' scope and seledct '$newGroupMessageSubject' conversation.") + inboxPage.filterInbox("Inbox") + inboxPage.selectConversation(newGroupMessageSubject) + + Log.d(STEP_TAG, "Delete the '$newGroupMessageSubject' conversation and assert that it has been removed from the 'INBOX' scope.") + inboxPage.clickDelete() + inboxPage.confirmDelete() + inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) + } + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) + fun testInboxSwipeGesturesE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 2, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student1 = data.studentsList[0] + val student2 = data.studentsList[1] - Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation and swipe it right to make it unread. Assert that the conversation became unread.") + val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) + val group = GroupsApi.createGroup(groupCategory.id, teacher.token) + Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} and ${student2.name} students to the group: ${group.name}.") + GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) + GroupsApi.createGroupMembership(group.id, student2.id, teacher.token) + + Log.d(STEP_TAG,"Login with user: ${student1.name}, login id: ${student1.loginId}.") + tokenLogin(student1) + dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course) + + Log.d(STEP_TAG,"Open Inbox Page. Assert that the previously seeded conversation is displayed.") + dashboardPage.clickInboxTab() + inboxPage.assertInboxEmpty() + + Log.d(PREPARATION_TAG,"Seed an email from the teacher to ${student1.name} and ${student2.name} students.") + val seededConversation = createConversation(teacher, student1, student2)[0] + + Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") + refresh() + inboxPage.assertHasConversation() + inboxPage.assertConversationDisplayed(seededConversation) + + Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation and swipe it right to make it read. Assert that the conversation became read.") inboxPage.selectConversation(seededConversation.subject) inboxPage.swipeConversationRight(seededConversation) - inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) + inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) - Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation and swipe it right again to make it read. Assert that the conversation became read.") + Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation and swipe it right again to make it unread. Assert that the conversation became unread.") inboxPage.swipeConversationRight(seededConversation) - inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) + inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'INBOX' scope because it has became archived.") inboxPage.swipeConversationLeft(seededConversation) @@ -271,51 +335,86 @@ class InboxE2ETest: StudentTest() { inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seededConversation.subject) - Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") - inboxPage.selectConversations(listOf(seededConversation.subject, newGroupMessageSubject)) + Log.d(STEP_TAG, "Select the conversation. Star it and mark it unread. (Preparing for swipe gestures in 'STARRED' and 'UNREAD' scope.") + inboxPage.selectConversations(listOf(seededConversation.subject)) + inboxPage.assertSelectedConversationNumber("1") inboxPage.clickStar() - inboxPage.assertSelectedConversationNumber("2") + inboxPage.assertConversationStarred(seededConversation.subject) inboxPage.clickMarkAsUnread() sleep(1000) - Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") + Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that the conversation is displayed in the 'STARRED' scope.") inboxPage.filterInbox("Starred") inboxPage.assertConversationDisplayed(seededConversation.subject) - inboxPage.assertConversationDisplayed(newGroupMessageSubject) Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") inboxPage.swipeConversationLeft(seededConversation) inboxPage.assertConversationNotDisplayed(seededConversation.subject) - Log.d(STEP_TAG, "Assert that '$newGroupMessageSubject' conversation is unread.") - inboxPage.assertUnreadMarkerVisibility(newGroupMessageSubject, ViewMatchers.Visibility.VISIBLE) - - Log.d(STEP_TAG, "Swipe '$newGroupMessageSubject' conversation right and assert that it has became read.") - inboxPage.swipeConversationRight(newGroupMessageSubject) - inboxPage.assertUnreadMarkerVisibility(newGroupMessageSubject, ViewMatchers.Visibility.GONE) - - Log.d(STEP_TAG, "Navigate to 'UNREAD' scope. Assert that only the '${seededConversation.subject}' conversation is displayed in the 'UNREAD' scope.") + Log.d(STEP_TAG, "Navigate to 'UNREAD' scope. Assert that the conversation is displayed in the 'Unread' scope.") inboxPage.filterInbox("Unread") inboxPage.assertConversationDisplayed(seededConversation.subject) - inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) - Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' conversation left and assert it has been removed from the 'UNREAD' scope since it has became read.") - inboxPage.swipeConversationLeft(seededConversation) + Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' conversation right and assert that it has disappeared from the 'UNREAD' scope.") + inboxPage.swipeConversationRight(seededConversation.subject) inboxPage.assertConversationNotDisplayed(seededConversation.subject) + } - Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seededConversation.subject}' conversation is displayed in the 'ARCHIVED' scope.") - inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(seededConversation.subject) + @E2E + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.E2E) + fun testHelpMenuAskYourInstructorMessage() { - Log.d(STEP_TAG, "Navigate to 'INBOX' scope and seledct '$newGroupMessageSubject' conversation.") - inboxPage.filterInbox("Inbox") - inboxPage.selectConversation(newGroupMessageSubject) + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 2, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student = data.studentsList[0] - Log.d(STEP_TAG, "Delete the '$newGroupMessageSubject' conversation and assert that it has been removed from the 'INBOX' scope.") - inboxPage.clickDelete() - inboxPage.confirmDelete() - inboxPage.assertConversationNotDisplayed(newGroupMessageSubject) + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open Help Menu.") + leftSideNavigationDrawerPage.clickHelpMenu() + + Log.d(STEP_TAG, "Assert Help Menu Dialog is displayed.") + helpPage.assertHelpMenuDisplayed() + + val questionText = "Can you see message this Instructor?" + val recipientList = student.shortName + ", " + teacher.shortName + + Log.d(STEP_TAG, "Send the '$questionText' question to the instructor (${teacher.shortName}) from the student (${student.shortName}).") + helpPage.sendQuestionToInstructor(course, questionText) + + Log.d(STEP_TAG, "Dismiss 'Ask Your Instructor' dialog. Open Inbox Page. Navigate to 'SENT' scope and assert that the conversations is displayed there with the proper recipients.") + Espresso.pressBack() + dashboardPage.clickInboxTab() + inboxPage.filterInbox("Sent") + inboxPage.assertConversationWithRecipientsDisplayed(recipientList) + + Log.d(STEP_TAG, "Open the conversation and assert that the message body is equal to which the student asked in the 'Ask Your Instructor' dialog: '$questionText'.") + inboxPage.openConversationWithRecipients(recipientList) + inboxConversationPage.assertMessageDisplayed(questionText) + + Log.d(STEP_TAG,"Log out with ${student.name} student.") + Espresso.pressBack() + leftSideNavigationDrawerPage.logout() + + Log.d(STEP_TAG,"Login with user: ${teacher.name}, login id: ${teacher.loginId}.") + tokenLogin(teacher) + dashboardPage.waitForRender() + + Log.d(STEP_TAG,"Open Inbox Page. Assert that the asked question is displayed in the teacher's inbox with the proper recipients ($recipientList) and message ($questionText).") + dashboardPage.clickInboxTab() + inboxPage.assertConversationWithRecipientsDisplayed(recipientList) + inboxPage.assertConversationDisplayed(questionText) + + Log.d(STEP_TAG, "Open the conversation and assert that there is no subject of the conversation and the message body is equal to which the student typed in the 'Ask Your Instructor' dialog: '$questionText'.") + inboxPage.openConversationWithRecipients(recipientList) + inboxConversationPage.assertMessageDisplayed(questionText) + inboxConversationPage.assertNoSubjectDisplayed() } private fun createConversation( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt new file mode 100644 index 0000000000..d40df12d12 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.offline + +import android.util.Log +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class DashboardE2EOfflineTest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E) + fun testOfflineDashboardE2E() { + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) + val student = data.studentsList[0] + val course1 = data.coursesList[0] + val course2 = data.coursesList[1] + val testAnnouncement = data.announcementsList[0] + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") + manageOfflineContentPage.selectEntireCourseForSync(course1.name) + manageOfflineContentPage.clickOnSyncButton() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + OfflineTestUtils.turnOffConnectionViaADB() + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") + OfflineTestUtils.assertOfflineIndicator() + + Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's cours card.") + dashboardPage.assertCourseOfflineSyncIconVisible(course1.name) + dashboardPage.assertCourseOfflineSyncIconGone(course2.name) + + Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") + dashboardPage.selectCourse(course1) + courseBrowserPage.selectAnnouncements() + + Log.d(STEP_TAG,"Assert that the '${testAnnouncement.title}' titled announcement is displayed, so the user is able to see it in offline mode because it was synced.") + announcementListPage.assertTopicDisplayed(testAnnouncement.title) + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") + OfflineTestUtils.turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt new file mode 100644 index 0000000000..7c3413a09f --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.offline + +import android.util.Log +import androidx.test.espresso.Espresso +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class OfflineSyncProgressE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SYNC_PROGRESS, TestCategory.E2E) + fun testOfflineGlobalCourseSyncProgressE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) + val student = data.studentsList[0] + val course1 = data.coursesList[0] + val course2 = data.coursesList[1] + val testAnnouncement = data.announcementsList[0] + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") + manageOfflineContentPage.selectEntireCourseForSync(course1.name) + manageOfflineContentPage.clickOnSyncButton() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and click on it to enter the Sync Progress Page.") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.clickOnSyncProgressNotification() + + Log.d(STEP_TAG, "Assert that the Sync Progress has started.") + syncProgressPage.waitForDownloadStarting() + + Log.d(STEP_TAG, "Assert that the Sync Progress has been successful (so to have the success title and the course success indicator).") + syncProgressPage.assertDownloadProgressSuccessDetails() + syncProgressPage.assertCourseSyncedSuccessfully(course1.name) + + Log.d(STEP_TAG, "Navigate back to Dashboard Page and wait for it to be rendered.") + Espresso.pressBack() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + OfflineTestUtils.turnOffConnectionViaADB() + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") + OfflineTestUtils.assertOfflineIndicator() + + Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.") + dashboardPage.assertCourseOfflineSyncIconVisible(course1.name) + dashboardPage.assertCourseOfflineSyncIconGone(course2.name) + + Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") + dashboardPage.selectCourse(course1) + courseBrowserPage.selectAnnouncements() + + Log.d(STEP_TAG,"Assert that the '${testAnnouncement.title}' titled announcement is displayed, so the user is able to see it in offline mode because it was synced.") + announcementListPage.assertTopicDisplayed(testAnnouncement.title) + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") + OfflineTestUtils.turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt new file mode 100644 index 0000000000..717d37af98 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.offline.utils + +import androidx.test.espresso.matcher.ViewMatchers.withChild +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView +import com.instructure.espresso.page.plus +import com.instructure.student.R +import org.hamcrest.CoreMatchers.allOf + +object OfflineTestUtils { + + fun turnOffConnectionViaADB() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.executeShellCommand("svc wifi disable") + device.executeShellCommand("svc data disable") + } + + fun turnOnConnectionViaADB() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.executeShellCommand("svc wifi enable") + device.executeShellCommand("svc data enable") + } + + fun turnOffConnectionOnUI() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + device.swipe(0, device.displayHeight / 2, 0, 0, 10) + + Thread.sleep(1000) + + val settingsApp = device.findObject(UiSelector().text("Settings")) + settingsApp.clickAndWaitForNewWindow() + + val scrollable = UiScrollable(UiSelector().scrollable(true)) + scrollable.scrollTextIntoView("Network & internet") + + val networkInternet = device.findObject(UiSelector().text("Network & internet")) + networkInternet.clickAndWaitForNewWindow() + + val wifiSwitch = + device.findObject(UiSelector().resourceId("com.android.settings:id/switch_widget")) + if (wifiSwitch.isChecked) { + wifiSwitch.click() + } + + val airplaneMode = device.findObject(UiSelector().text("Airplane mode")) + airplaneMode.click() + + device.pressHome() + } + + fun assertOfflineIndicator() { + waitForView( + withId(R.id.offlineIndicator) + allOf( + withChild(withId(R.id.divider)), + withChild(withId(R.id.offlineIndicatorIcon)), + withChild(withId(R.id.offlineIndicatorText) + withText(R.string.offline)) + ) + ).assertDisplayed() + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt index 56568c7fa7..f02805dfff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt @@ -16,6 +16,7 @@ */ package com.instructure.student.ui.interaction +import androidx.lifecycle.MutableLiveData import androidx.test.espresso.Espresso import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addAccountNotification @@ -27,16 +28,34 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.di.NetworkStateProviderModule +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.espresso.fakes.FakeNetworkStateProvider import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules import junit.framework.TestCase.assertNotNull +import org.junit.Before import org.junit.Test +@UninstallModules(NetworkStateProviderModule::class) @HiltAndroidTest class DashboardInteractionTest : StudentTest() { override fun displaysPageObjects() = Unit // Not used for interaction tests + private val isOnlineLiveData = MutableLiveData() + + @BindValue + @JvmField + val networkStateProvider: NetworkStateProvider = FakeNetworkStateProvider(isOnlineLiveData) + + @Before + fun setUp() { + isOnlineLiveData.postValue(true) + } + @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.INTERACTION) fun testNavigateToDashboard() { @@ -281,7 +300,27 @@ class DashboardInteractionTest : StudentTest() { leftSideNavigationDrawerPage.setShowGrades(true) dashboardPage.assertGradeText("A") } - + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + fun testOfflineIndicatorDisplayedIfOffline() { + goToDashboard(setUpData()) + + isOnlineLiveData.postValue(false) + + dashboardPage.assertOfflineIndicatorDisplayed() + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + fun testOfflineIndicatorNotDisplayedIfOnline() { + goToDashboard(setUpData()) + + isOnlineLiveData.postValue(true) + + dashboardPage.assertOfflineIndicatorNotDisplayed() + } + private fun setUpData( courseCount: Int = 1, invitedCourseCount: Int = 0, @@ -295,16 +334,17 @@ class DashboardInteractionTest : StudentTest() { invitedCourseCount = invitedCourseCount, pastCourseCount = pastCourseCount, favoriteCourseCount = favoriteCourseCount, - accountNotificationCount = announcementCount) + accountNotificationCount = announcementCount + ) } - + private fun goToDashboard(data: MockCanvas) { val student = data.students[0] val token = data.tokenFor(student)!! tokenLogin(data.domain, token, student) dashboardPage.waitForRender() } - + private fun setUpCustomGrade(grade: String, score: Double, data: MockCanvas, restrictQuantitativeData: Boolean) { val student = data.students[0] val course = data.courses.values.first() @@ -312,9 +352,10 @@ class DashboardInteractionTest : StudentTest() { val enrollment = course.enrollments!!.first { it.userId == student.id } .copy(grades = Grades(currentGrade = grade, currentScore = score)) - val newCourse = course - .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData), - enrollments = mutableListOf(enrollment)) + val newCourse = course.copy( + settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData), + enrollments = mutableListOf(enrollment) + ) data.courses[course.id] = newCourse } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index 394cab7b96..a0e3049b48 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -19,8 +19,18 @@ package com.instructure.student.ui.interaction import android.os.SystemClock.sleep import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.mockCanvas.* -import com.instructure.canvasapi2.models.* +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addAssignment +import com.instructure.canvas.espresso.mockCanvas.addDiscussionTopicToCourse +import com.instructure.canvas.espresso.mockCanvas.addFileToCourse +import com.instructure.canvas.espresso.mockCanvas.addReplyToDiscussion +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.RemoteFile +import com.instructure.canvasapi2.models.Tab import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -435,7 +445,7 @@ class DiscussionsInteractionTest : StudentTest() { course = course1, user = user, topicTitle = "Hey! A Discussion!", - topicDescription = "Awesome!" + topicDescription = "Awesome!", ) courseBrowserPage.selectDiscussions() @@ -466,8 +476,9 @@ class DiscussionsInteractionTest : StudentTest() { val attachment = createHtmlAttachment(data, attachmentHtml) discussionEntry.attachments = mutableListOf(attachment) - discussionDetailsPage.refresh() - Thread.sleep(3000) //allow some time to the reply to propagate + Espresso.pressBack() + discussionListPage.selectTopic(topicHeader.title!!) + discussionDetailsPage.assertReplyDisplayed(discussionEntry) discussionDetailsPage.assertReplyAttachment(discussionEntry) discussionDetailsPage.previewAndCheckReplyAttachment( @@ -565,8 +576,9 @@ class DiscussionsInteractionTest : StudentTest() { val attachment = createHtmlAttachment(data, attachmentHtml) replyReplyEntry.attachments = mutableListOf(attachment) - discussionDetailsPage.refresh() // To pick up updated discussion reply - Thread.sleep(3000) //Need this because somehow sometimes refresh does "double-refresh" and assert is failing below. + Espresso.pressBack() + discussionListPage.selectTopic(topicHeader.title!!) + discussionDetailsPage.assertReplyDisplayed(replyReplyEntry) discussionDetailsPage.assertReplyAttachment(replyReplyEntry) discussionDetailsPage.previewAndCheckReplyAttachment( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt index 91cd8158c0..3fd8e2f5ff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ImportantDatesInteractionTest.kt @@ -196,9 +196,10 @@ class ImportantDatesInteractionTest : StudentTest() { importantDatesPage.assertDayTextIsDisplayed(generateDayString(todayEvent.startDate)) importantDatesPage.assertDayTextIsDisplayed(generateDayString(twoDaysFromNowEvent.startDate)) + importantDatesPage.assertItemDisplayed(generateDayString(threeDaysFromNowEvent.startDate)) + importantDatesPage.assertRecyclerViewItemCount(3) importantDatesPage.swipeUp() // Need to do this because on landscape mode the last item cannot be seen on the view by default. importantDatesPage.assertDayTextIsDisplayed(generateDayString(threeDaysFromNowEvent.startDate)) - importantDatesPage.assertRecyclerViewItemCount(3) } private fun goToImportantDatesTab(data: MockCanvas) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt index d79fbf11f1..17ba053fe2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NavigationDrawerInteractionTest.kt @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.content.Intent import android.os.Build import android.util.Log +import androidx.lifecycle.MutableLiveData import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers import com.instructure.canvas.espresso.mockCanvas.MockCanvas @@ -34,15 +35,21 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.di.NetworkStateProviderModule +import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.R +import com.instructure.student.espresso.fakes.FakeNetworkStateProvider import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLogin import com.instructure.student.ui.utils.tokenLoginElementary +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules import org.hamcrest.CoreMatchers import org.junit.Before import org.junit.Test +@UninstallModules(NetworkStateProviderModule::class) @HiltAndroidTest class NavigationDrawerInteractionTest : StudentTest() { override fun displaysPageObjects() = Unit // Not used for interaction tests @@ -53,13 +60,19 @@ class NavigationDrawerInteractionTest : StudentTest() { private lateinit var activity: Activity + private val isOnlineLiveData = MutableLiveData() + + @BindValue + @JvmField + val networkStateProvider: NetworkStateProvider = FakeNetworkStateProvider(isOnlineLiveData) + @Before fun setUp() { // If we try to read this later, it may be null, possibly because we will have navigated // away from our initial activity. activity = activityRule.activity - + isOnlineLiveData.postValue(true) } // Should be able to change the user from the navigation drawer @@ -69,7 +82,7 @@ class NavigationDrawerInteractionTest : StudentTest() { // This test fails on API-28 in FTL due to a "TOO_MANY_REGISTRATIONS" issue on logout. // IMO, this is not something that we can fix. So let's not run the test. - if(Build.VERSION.SDK_INT == 28) { + if (Build.VERSION.SDK_INT == 28) { return } @@ -78,7 +91,8 @@ class NavigationDrawerInteractionTest : StudentTest() { // Need to remember student1 via PreviousUserUtils in order to be able to "change user" // back to student1. - PreviousUsersUtils.add(ContextKeeper.appContext, SignedInUser( + PreviousUsersUtils.add( + ContextKeeper.appContext, SignedInUser( user = student1, domain = data.domain, protocol = ApiPrefs.protocol, @@ -88,7 +102,8 @@ class NavigationDrawerInteractionTest : StudentTest() { clientId = "", clientSecret = "", calendarFilterPrefs = null - )) + ) + ) leftSideNavigationDrawerPage.clickChangeUserMenu() // Sign in student 2 @@ -113,7 +128,7 @@ class NavigationDrawerInteractionTest : StudentTest() { // This test fails on API-28 in FTL due to a "TOO_MANY_REGISTRATIONS" issue on logout. // IMO, this is not something that we can fix. So let's not run the test. - if(Build.VERSION.SDK_INT == 28) { + if (Build.VERSION.SDK_INT == 28) { return } @@ -179,7 +194,7 @@ class NavigationDrawerInteractionTest : StudentTest() { val activities = pkgMgr.queryIntentActivities(intent, 0) val matchedChooserActivities = activities.count() for (activity in activities) { - Log.d("submitFeatureIdea","Resolved activity = $activity") + Log.d("submitFeatureIdea", "Resolved activity = $activity") } Intents.init() @@ -192,12 +207,12 @@ class NavigationDrawerInteractionTest : StudentTest() { // Formulate what an actual email intent (NOT a chooser intent) would look like val emailIntentMatcher = CoreMatchers.allOf( - IntentMatchers.hasAction(Intent.ACTION_SEND), - IntentMatchers.hasType("message/rfc822"), - CoreMatchers.anyOf( - IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("support@instructure.com")), - IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("mobilesupport@instructure.com")) - ) + IntentMatchers.hasAction(Intent.ACTION_SEND), + IntentMatchers.hasType("message/rfc822"), + CoreMatchers.anyOf( + IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("support@instructure.com")), + IntentMatchers.hasExtra(Intent.EXTRA_EMAIL, arrayOf("mobilesupport@instructure.com")) + ) ) // Set up our intent catchers @@ -209,8 +224,7 @@ class NavigationDrawerInteractionTest : StudentTest() { // :-( Our production code creates a chooser every time, even if there is only one email app option... Intents.intended(chooserIntentMatcher) - } - finally { + } finally { Intents.release() } } @@ -225,18 +239,17 @@ class NavigationDrawerInteractionTest : StudentTest() { Intents.init() try { val expectedIntent = CoreMatchers.allOf( - IntentMatchers.hasAction(Intent.ACTION_VIEW), - CoreMatchers.anyOf( - // Could be either of these, depending on whether the play store app is installed - IntentMatchers.hasData("market://details?id=com.instructure.candroid"), - IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid") - ) + IntentMatchers.hasAction(Intent.ACTION_VIEW), + CoreMatchers.anyOf( + // Could be either of these, depending on whether the play store app is installed + IntentMatchers.hasData("market://details?id=com.instructure.candroid"), + IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid") + ) ) Intents.intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null)) helpPage.shareYourLove() Intents.intended(expectedIntent) - } - finally { + } finally { Intents.release() } } @@ -257,10 +270,30 @@ class NavigationDrawerInteractionTest : StudentTest() { leftSideNavigationDrawerPage.assertMenuItems(true) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + fun testOfflineIndicatorDisplayedIfOffline() { + signInStudent() + + isOnlineLiveData.postValue(false) + + leftSideNavigationDrawerPage.assertOfflineIndicatorDisplayed() + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + fun testOfflineIndicatorNotDisplayedIfOnline() { + signInStudent() + + isOnlineLiveData.postValue(true) + + leftSideNavigationDrawerPage.assertOfflineIndicatorNotDisplayed() + } + /** * Create two mocked students, sign in the first one, end up on the dashboard page */ - private fun signInStudent(courseCount: Int = 1, studentCount: Int = 2, favoriteCourseCount: Int = 1) : MockCanvas { + private fun signInStudent(courseCount: Int = 1, studentCount: Int = 2, favoriteCourseCount: Int = 1): MockCanvas { val data = MockCanvas.init( studentCount = studentCount, courseCount = courseCount, @@ -280,17 +313,19 @@ class NavigationDrawerInteractionTest : StudentTest() { } private fun signInElementaryStudent( - courseCount: Int = 1, - pastCourseCount: Int = 0, - favoriteCourseCount: Int = 0, - announcementCount: Int = 0): MockCanvas { + courseCount: Int = 1, + pastCourseCount: Int = 0, + favoriteCourseCount: Int = 0, + announcementCount: Int = 0 + ): MockCanvas { val data = MockCanvas.init( - studentCount = 1, - courseCount = courseCount, - pastCourseCount = pastCourseCount, - favoriteCourseCount = favoriteCourseCount, - accountNotificationCount = announcementCount) + studentCount = 1, + courseCount = courseCount, + pastCourseCount = pastCourseCount, + favoriteCourseCount = favoriteCourseCount, + accountNotificationCount = announcementCount + ) val student = data.students[0] val token = data.tokenFor(student)!! diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt index 2e38bf2566..dc0d06eb3b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ScheduleInteractionTest.kt @@ -31,7 +31,7 @@ import com.instructure.panda_annotations.TestMetaData import com.instructure.pandautils.utils.date.DateTimeProvider import com.instructure.student.R import com.instructure.student.ui.pages.ElementaryDashboardPage -import com.instructure.student.ui.utils.FakeDateTimeProvider +import com.instructure.student.ui.utils.di.FakeDateTimeProvider import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.tokenLoginElementary import dagger.hilt.android.testing.HiltAndroidTest diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt index 4956022e41..4238f38ec8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SettingsInteractionTest.kt @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.content.Intent import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers +import com.instructure.canvas.espresso.StubMultiAPILevel import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.Course @@ -87,6 +88,7 @@ class SettingsInteractionTest : StudentTest() { // Should display the privacy policy in a WebView @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + @StubMultiAPILevel("Failed API levels = { 28 }", "Somehow the Privacy Policy URL does not load on API lvl 28, but does on other API lvl devices.") fun testLegal_showPrivacyPolicy() { setUpAndSignIn() @@ -114,14 +116,35 @@ class SettingsInteractionTest : StudentTest() { pairObserverPage.hasCode("2") } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + fun testOfflineContent_notDisplayedIfFeatureIsDisabled() { + setUpAndSignIn(offlineEnabled = false) + + leftSideNavigationDrawerPage.clickSettingsMenu() + settingsPage.assertOfflineContentNotDisplayed() + } + + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SETTINGS, TestCategory.INTERACTION, false) + fun testOfflineContent_displayedIfFeatureIsEnabled() { + setUpAndSignIn(offlineEnabled = true) + + leftSideNavigationDrawerPage.clickSettingsMenu() + settingsPage.assertOfflineContentDisplayed() + } + // Mock a single student and course, sign in, then navigate to the dashboard. - private fun setUpAndSignIn(): MockCanvas { + private fun setUpAndSignIn(offlineEnabled: Boolean = false): MockCanvas { // Basic info val data = MockCanvas.init( - studentCount = 1, - courseCount = 1, - favoriteCourseCount = 1) + studentCount = 1, + courseCount = 1, + favoriteCourseCount = 1 + ) + + data.offlineModeEnabled = offlineEnabled course = data.courses.values.first() // Sign in @@ -132,5 +155,4 @@ class SettingsInteractionTest : StudentTest() { return data } - } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt new file mode 100644 index 0000000000..e81a192070 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.ui.interaction + +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.R +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Test + +@HiltAndroidTest +class SyncSettingsInteractionTest : StudentTest() { + + override fun displaysPageObjects() = Unit + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) + fun testFurtherSettingsDisplayedByDefault() { + goToSyncSettings() + syncSettingsPage.assertFurtherSettingsIsDisplayed() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) + fun testClickAutoSyncHidesFurtherSettings() { + goToSyncSettings() + syncSettingsPage.clickAutoSyncSwitch() + syncSettingsPage.assertFurtherSettingsNotDisplayed() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) + fun testChangeFrequency() { + goToSyncSettings() + syncSettingsPage.assertFrequencyLabelText(R.string.daily) + syncSettingsPage.clickFrequency() + syncSettingsPage.clickDialogOption(R.string.weekly) + syncSettingsPage.assertFrequencyLabelText(R.string.weekly) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) + fun testChangeContentOverWifiOnly() { + goToSyncSettings() + syncSettingsPage.assertWifiOnlySwitchIsChecked() + syncSettingsPage.clickWifiOnlySwitch() + syncSettingsPage.assertDialogDisplayedWithTitle(R.string.syncSettings_wifiConfirmationTitle) + syncSettingsPage.clickTurnOff() + syncSettingsPage.assertWifiOnlySwitchIsNotChecked() + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) + fun testChangesSavedCorrectly() { + val data = createMockCanvas() + goToSyncSettings(data) + + syncSettingsPage.clickFrequency() + syncSettingsPage.clickDialogOption(R.string.weekly) + syncSettingsPage.clickWifiOnlySwitch() + syncSettingsPage.clickTurnOff() + + with(activityRule) { + finishActivity() + launchActivity(null) + } + + goToSyncSettings(data) + + syncSettingsPage.assertFrequencyLabelText(R.string.weekly) + syncSettingsPage.assertWifiOnlySwitchIsNotChecked() + } + + private fun createMockCanvas(): MockCanvas { + val data = MockCanvas.init(studentCount = 1, teacherCount = 1, courseCount = 1) + data.offlineModeEnabled = true + return data + } + + private fun goToSyncSettings(data: MockCanvas = createMockCanvas()) { + val student = data.students.first() + val token = data.tokenFor(student).orEmpty() + tokenLogin(data.domain, token, student) + dashboardPage.waitForRender() + leftSideNavigationDrawerPage.clickSettingsMenu() + settingsPage.openOfflineContentPage() + } +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt index d4986a1d9e..26c6b09593 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AnnouncementListPage.kt @@ -17,16 +17,16 @@ package com.instructure.student.ui.pages import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import com.instructure.espresso.Searchable import com.instructure.espresso.assertDisplayed import com.instructure.espresso.matchers.WaitForViewMatcher -import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.plus import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.student.R -class AnnouncementListPage() : BasePage(R.id.discussionListPage) { +class AnnouncementListPage(searchable: Searchable) : DiscussionListPage(searchable) { fun assertToolbarTitle() { WaitForViewMatcher.waitForView(withParent(R.id.discussionListToolbar) + withText(R.string.announcements)).assertDisplayed() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt index 6d089573a3..d6550170a6 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/BookmarkPage.kt @@ -27,11 +27,11 @@ import com.instructure.espresso.assertDisplayed import com.instructure.espresso.clearText import com.instructure.espresso.click import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.waitForView import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf class BookmarkPage : BasePage() { @@ -79,6 +79,6 @@ class BookmarkPage : BasePage() { fun deleteBookmark(bookmarkName: String) { clickOnMoreMenu(bookmarkName) onView(allOf(withId(R.id.title), withText("Delete"), isDisplayed())).click() - onView(withText(R.string.ok) + withAncestor(R.id.buttonPanel)).click() + waitForView(anyOf(withText(android.R.string.ok), withText(R.string.ok))).click() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt index 3261c98c36..58f45c6131 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseGradesPage.kt @@ -19,6 +19,7 @@ package com.instructure.student.ui.pages import android.os.SystemClock.sleep import android.view.View import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.assertion.ViewAssertions @@ -36,10 +37,10 @@ import com.instructure.espresso.assertDisplayed import com.instructure.espresso.assertHasText import com.instructure.espresso.assertNotDisplayed import com.instructure.espresso.click -import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.withAncestor import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent @@ -49,7 +50,7 @@ import com.instructure.espresso.typeText import com.instructure.student.R import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf -import java.util.concurrent.TimeUnit +import java.util.concurrent.* class CourseGradesPage : BasePage(R.id.courseGradesPage) { private val gradeLabel by WaitForViewWithId(R.id.txtOverallGradeLabel) @@ -82,20 +83,25 @@ class CourseGradesPage : BasePage(R.id.courseGradesPage) { } fun assertAssignmentDisplayed(name: String, gradeString: String) { - onView(withId(R.id.title) + withParent(R.id.textContainer)).assertHasText(name) - val siblingMatcher = withId(R.id.title) + withText(name) - onView(withId(R.id.points) + hasSibling(siblingMatcher)).assertHasText(gradeString) + val siblingMatcher = withId(R.id.title) + withParent(R.id.textContainer) + withText(name) + withAncestor(R.id.courseGradesPage) + onView(withId(R.id.points) + hasSibling(siblingMatcher)).scrollTo().assertHasText(gradeString) } // Hopefully this will be sufficient. We may need to add some logic to scroll // to the top of the list first. We have to use the custom constraints because the // swipeRefreshLayout may extend below the screen, and therefore may not be 90% visible. fun refresh() { - onView(allOf(withId(R.id.swipeRefreshLayout), isDisplayed())) + onView(allOf(withId(R.id.swipeRefreshLayout), withAncestor(R.id.courseGradesPage), isDisplayed())) .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(5))) sleep(1000) // Allow some time to react to the update. } + fun swipeUp() { + onView(allOf(withId(R.id.swipeRefreshLayout), withAncestor(R.id.courseGradesPage), isDisplayed())) + .perform(withCustomConstraints(ViewActions.swipeUp(), isDisplayingAtLeast(5))) + sleep(1000) // Allow some time to react to the update. + } + // TODO: Explicitly check or un-check, rather than assuming current state fun toggleWhatIf() { showWhatIfCheckbox.perform(click()) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index caa43663b0..116ea38c00 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -29,6 +29,7 @@ import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.platform.app.InstrumentationRegistry import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.AccountNotification @@ -39,6 +40,7 @@ import com.instructure.dataseeding.model.GroupApiModel import com.instructure.espresso.* import com.instructure.espresso.page.* import com.instructure.student.R +import com.instructure.student.ui.utils.ViewUtils import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString import org.hamcrest.Matcher @@ -60,7 +62,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(withParent(R.id.toolbar) + withText(R.string.dashboard)).assertDisplayed() listView.assertDisplayed() onViewWithText("Courses").assertDisplayed() - onViewWithText("Edit Dashboard").assertDisplayed() + onViewWithText("All Courses").assertDisplayed() } fun assertDisplaysCourse(course: CourseApiModel) { @@ -68,8 +70,8 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun assertDisplaysCourse(courseName: String) { - val matcher = allOf(withText(courseName), withId(R.id.titleTextView), withAncestor(R.id.dashboardPage)) - scrollAndAssertDisplayed(matcher) + val matcher = allOf(withText(courseName), withId(R.id.titleTextView), withAncestor(R.id.dashboardPage)) + waitForView(matcher).scrollTo().assertDisplayed() } fun assertDisplaysCourse(course: Course) { @@ -78,8 +80,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { // This is the RIGHT way to do it, but it inexplicably fails most of the time. scrollRecyclerView(R.id.listView, matcher) onView(matcher).assertDisplayed() - } - catch(pe: PerformException) { + } catch (pe: PerformException) { // Revert to this weaker operation if the one above fails. scrollAndAssertDisplayed(matcher) } @@ -103,16 +104,16 @@ class DashboardPage : BasePage(R.id.dashboardPage) { private fun assertDisplaysGroupCommon(groupName: String, courseName: String) { val groupNameMatcher = allOf(withText(groupName), withId(R.id.groupNameView)) - onView(groupNameMatcher).scrollTo().assertDisplayed() + waitForView(groupNameMatcher).scrollTo().assertDisplayed() val groupDescriptionMatcher = allOf(withText(courseName), withId(R.id.groupCourseView), hasSibling(groupNameMatcher)) - onView(groupDescriptionMatcher).scrollTo().assertDisplayed() + waitForView(groupDescriptionMatcher).scrollTo().assertDisplayed() } fun assertDisplaysAddCourseMessage() { emptyView.assertDisplayed() - onViewWithText(R.string.welcome).assertDisplayed() - onViewWithText(R.string.emptyCourseListMessage).assertDisplayed() - onViewWithId(R.id.addCoursesButton).assertDisplayed() + waitForViewWithText(R.string.welcome).assertDisplayed() + waitForViewWithText(R.string.emptyCourseListMessage).assertDisplayed() + waitForViewWithId(R.id.addCoursesButton).assertDisplayed() } fun assertCourseLabelTextColor(expectedTextColor: String) { @@ -182,7 +183,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun selectGroup(group: Group) { val groupNameMatcher = allOf(withText(group.name), withId(R.id.groupNameView)) - onView(groupNameMatcher).scrollTo().click() + waitForView(groupNameMatcher).scrollTo().click() } fun selectCourse(course: CourseApiModel) { @@ -254,7 +255,15 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun switchCourseView() { - onView(ViewMatchers.withId(R.id.menu_dashboard_cards)).click() + Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) + onView(withText(containsString("Switch to"))) + .perform(click()); + } + + fun openGlobalManageOfflineContentPage() { + Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) + onView(withText(containsString("Manage Offline Content"))) + .perform(click()); } fun clickEditDashboard() { @@ -294,7 +303,10 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun clickCourseOverflowMenu(courseTitle: String, menuTitle: String) { - val courseOverflowMatcher = withId(R.id.overflow) + withAncestor(withId(R.id.cardView) + withDescendant(withId(R.id.titleTextView) + withText(courseTitle))) + val courseOverflowMatcher = withId(R.id.overflow) + withAncestor( + withId(R.id.cardView) + + withDescendant(withId(R.id.titleTextView) + withText(courseTitle)) + ) onView(courseOverflowMatcher).scrollTo().click() waitForView(withId(R.id.title) + withText(menuTitle)).click() } @@ -321,6 +333,51 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun clickOnDashboardNotification(subTitle: String) { onView(withId(R.id.uploadSubtitle) + withText(subTitle)).click() } + + //OfflineMethod + fun assertOfflineIndicatorDisplayed() { + waitForView(withId(R.id.offlineIndicator)).assertDisplayed() + } + + //OfflineMethod + fun assertOfflineIndicatorNotDisplayed() { + onView(withId(R.id.offlineIndicator)).check(matches(withEffectiveVisibility(Visibility.GONE))) + } + + //OfflineMethod + fun assertCourseOfflineSyncIconVisible(courseName: String) { + waitForView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.titleTextView) + withText(courseName))).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } + + //OfflineMethod + fun assertCourseOfflineSyncIconGone(courseName: String) { + onView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.titleTextView) + withText(courseName))).check(matches(withEffectiveVisibility(Visibility.GONE))) + } + + //OfflineMethod + fun clickOnSyncProgressNotification() { + waitForView(ViewMatchers.withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent)).click() + } + + //OfflineMethod + fun waitForSyncProgressDownloadStartedNotificationToDisappear() { + ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_downloadStarting), 30) + } + + //OfflineMethod + fun waitForSyncProgressDownloadStartedNotification() { + waitForView(withText(com.instructure.pandautils.R.string.syncProgress_downloadStarting)).assertDisplayed() + } + + //OfflineMethod + fun waitForSyncProgressStartingNotification() { + waitForView(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent)).assertDisplayed() + } + + //OfflineMethod + fun waitForSyncProgressStartingNotificationToDisappear() { + ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) + } } /** @@ -329,7 +386,7 @@ class DashboardPage : BasePage(R.id.dashboardPage) { */ class SetSwitchCompat(val position: Boolean) : ViewAction { override fun getDescription(): String { - val desiredPosition = if(position) "On" else "Off" + val desiredPosition = if (position) "On" else "Off" return "Set SwitchCompat to $desiredPosition" } @@ -339,7 +396,7 @@ class SetSwitchCompat(val position: Boolean) : ViewAction { override fun perform(uiController: UiController?, view: View?) { val switch = view as SwitchCompat - if(switch != null) { + if (switch != null) { switch.isChecked = position } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt index c69c5c1484..364b5ac258 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionDetailsPage.kt @@ -26,16 +26,28 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.sugar.Web.onWebView -import androidx.test.espresso.web.webdriver.DriverAtoms.* +import androidx.test.espresso.web.webdriver.DriverAtoms.findElement +import androidx.test.espresso.web.webdriver.DriverAtoms.getText +import androidx.test.espresso.web.webdriver.DriverAtoms.webClick import androidx.test.espresso.web.webdriver.Locator -import com.instructure.canvas.espresso.* +import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.isElementDisplayed +import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvas.espresso.withCustomConstraints +import com.instructure.canvas.espresso.withElementRepeat import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.espresso.* +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertGone +import com.instructure.espresso.assertHasText +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.plus import com.instructure.espresso.page.waitForViewWithId import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.scrollTo import com.instructure.student.R import com.instructure.student.ui.utils.TypeInRCETextEditor import org.hamcrest.Matchers.allOf diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt index 3e6bebfce7..28f125e1a9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DiscussionListPage.kt @@ -49,7 +49,7 @@ import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.containsString -class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discussionListPage) { +open class DiscussionListPage(val searchable: Searchable) : BasePage(R.id.discussionListPage) { private val createNewDiscussion by OnViewWithId(R.id.createNewDiscussion) private val announcementsRecyclerView by OnViewWithId(R.id.discussionRecyclerView) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt index 6891b89897..c4bd4d94cf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/EditDashboardPage.kt @@ -104,14 +104,14 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) { fun selectAllCourses() { val childMatcher = withContentDescription("Add all to dashboard") - val itemMatcher = allOf(hasDescendant(withText("All courses")), hasDescendant(childMatcher)) + val itemMatcher = allOf(hasDescendant(withText(R.string.allCoursesCourseHeader)), hasDescendant(childMatcher)) onView(withParent(itemMatcher) + childMatcher).click() } fun unselectAllCourses() { val childMatcher = withContentDescription("Remove all from dashboard") - val itemMatcher = allOf(hasDescendant(withText("All courses")), hasDescendant(childMatcher)) + val itemMatcher = allOf(hasDescendant(withText(R.string.allCoursesCourseHeader)), hasDescendant(childMatcher)) onView(withParent(itemMatcher) + childMatcher).click() } @@ -120,13 +120,13 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) { if (someSelected) { val childMatcher = withContentDescription("Remove all from dashboard") - val itemMatcher = allOf(hasDescendant(withText("All courses")), hasDescendant(childMatcher)) + val itemMatcher = allOf(hasDescendant(withText(R.string.allCoursesCourseHeader)), hasDescendant(childMatcher)) onView(withParent(itemMatcher) + childMatcher).assertDisplayed() } else { val childMatcher = withContentDescription("Add all to dashboard") - val itemMatcher = allOf(hasDescendant(withText("All courses")), hasDescendant(childMatcher)) + val itemMatcher = allOf(hasDescendant(withText(R.string.allCoursesCourseHeader)), hasDescendant(childMatcher)) onView(withParent(itemMatcher) + childMatcher).assertDisplayed() } @@ -135,12 +135,12 @@ class EditDashboardPage : BasePage(R.id.editDashboardPage) { fun assertGroupMassSelectButtonIsDisplayed(someSelected: Boolean) { if (someSelected) { val itemMatcher = withContentDescription("Remove all from dashboard") - val parentMatcher = allOf(hasDescendant(withText("All groups")), hasDescendant(itemMatcher)) + val parentMatcher = allOf(hasDescendant(withText(R.string.allCoursesGroupHeader)), hasDescendant(itemMatcher)) onView(withParent(parentMatcher) + itemMatcher).scrollTo().assertDisplayed() } else { val itemMatcher = withContentDescription("Add all to dashboard") - val parentMatcher = allOf(hasDescendant(withText("All groups")), hasDescendant(itemMatcher)) + val parentMatcher = allOf(hasDescendant(withText(R.string.allCoursesGroupHeader)), hasDescendant(itemMatcher)) onView(withParent(parentMatcher) + itemMatcher).scrollTo().assertDisplayed() } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt index 7ebf099da4..1a5b4edd07 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/FileListPage.kt @@ -83,7 +83,7 @@ class FileListPage(val searchable: Searchable) : BasePage(R.id.fileListPage) { fun createNewFolder(folderName: String) { waitForViewWithId(R.id.textInput).typeText(folderName) - onView(withText(R.string.ok)).click() + onView(withText(android.R.string.ok)).click() } fun assertPdfPreviewDisplayed() { @@ -108,7 +108,7 @@ class FileListPage(val searchable: Searchable) : BasePage(R.id.fileListPage) { onView(withId(R.id.textInput)).clearText() onView(withId(R.id.textInput)).typeText(newName) onView(containsTextCaseInsensitive("OK")).click() - Espresso.pressBack() //Close soft keyboard + Espresso.closeSoftKeyboard() refresh() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt index b9083adfe1..224a53087f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/HelpPage.kt @@ -25,10 +25,16 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.withCustomConstraints import com.instructure.canvasapi2.models.Course -import com.instructure.espresso.* +import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.espresso.OnViewWithStringTextIgnoreCase +import com.instructure.espresso.OnViewWithText +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.plus +import com.instructure.espresso.scrollTo +import com.instructure.espresso.typeText import com.instructure.student.R // This is a little hokey, as the options that appear are somewhat governed by the results of @@ -50,6 +56,20 @@ class HelpPage : BasePage(R.id.helpDialog) { onView(containsTextCaseInsensitive("Send")).assertDisplayed() } + fun verifyAskAQuestion(course: CourseApiModel, question: String) { + askInstructorLabel.scrollTo().click() + waitForView(withText(course.name)).assertDisplayed() // Verify that our course is selected in the spinner + onView(withId(R.id.message)).scrollTo().perform(withCustomConstraints(typeText(question), isDisplayingAtLeast(1))) + Espresso.closeSoftKeyboard() + // Let's just make sure that the "Send" button is displayed, rather than actually pressing it + onView(containsTextCaseInsensitive("Send")).assertDisplayed() + } + + fun sendQuestionToInstructor(course: CourseApiModel, question: String) { + verifyAskAQuestion(course, question) + onView(containsTextCaseInsensitive("Send")).click() + } + fun launchGuides() { searchGuidesLabel.scrollTo().click() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt index c703f81202..959dee5ad9 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxConversationPage.kt @@ -24,29 +24,19 @@ import android.view.View import android.widget.ImageView import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.MenuPopupWindow -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.hasChildCount -import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withHint -import androidx.test.platform.app.InstrumentationRegistry -import com.instructure.canvas.espresso.containsTextCaseInsensitive -import com.instructure.canvas.espresso.explicitClick -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.stringContainsTextCaseInsensitive -import com.instructure.canvas.espresso.withCustomConstraints +import androidx.test.espresso.matcher.ViewMatchers.* +import com.instructure.canvas.espresso.* import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onViewWithContentDescription import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.waitForViewWithHint import com.instructure.espresso.page.waitForViewWithText @@ -59,7 +49,6 @@ import com.instructure.pandautils.utils.ThemePrefs import com.instructure.student.R import org.hamcrest.CoreMatchers import org.hamcrest.Description -import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf import org.hamcrest.TypeSafeMatcher @@ -83,20 +72,19 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) { } fun markUnread() { - onView(withContentDescription(stringContainsTextCaseInsensitive("More options"))).click() + onView(allOf(withContentDescription(stringContainsTextCaseInsensitive("More options")), isDisplayed())).click() onView(withText("Mark as Unread")).click() } fun archive() { - onView(withContentDescription(stringContainsTextCaseInsensitive("More options"))).click() + onView(allOf(withContentDescription(stringContainsTextCaseInsensitive("More options")), isDisplayed())).click() onView(withText("Archive")).click() } fun deleteConversation() { - onView(withContentDescription(stringContainsTextCaseInsensitive("More options"))).click() + onView(allOf(withContentDescription(stringContainsTextCaseInsensitive("More options")), isDisplayed())).click() onView(withText("Delete")).click() - onView(allOf(isAssignableFrom(AppCompatButton::class.java), containsTextCaseInsensitive("DELETE"))) - .click() // Confirmation click + onView(allOf(isAssignableFrom(AppCompatButton::class.java), containsTextCaseInsensitive("DELETE"))).click() // Confirmation click } fun deleteMessage(messageBody: String) { @@ -137,6 +125,10 @@ class InboxConversationPage : BasePage(R.id.inboxConversationPage) { onViewWithText(displayName).check(matches(isDisplayingAtLeast(5))) } + fun assertNoSubjectDisplayed() { + onView(withId(R.id.subjectView) + withText(R.string.noSubject)).assertDisplayed() + } + fun refresh() { onView(allOf(ViewMatchers.withId(R.id.swipeRefreshLayout), isDisplayingAtLeast(10))) .perform(withCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(10))) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt index 223ceaa4de..47ac688d03 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt @@ -73,6 +73,11 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).scrollTo().assertDisplayed() } + fun assertConversationWithRecipientsDisplayed(recipients: String) { + val matcher = withId(R.id.userName) + withAncestor(R.id.inboxRecyclerView) + withText(recipients) + onView(matcher).scrollTo().assertDisplayed() + } + fun assertConversationNotDisplayed(conversation: ConversationApiModel) { assertConversationNotDisplayed(conversation.subject) } @@ -95,6 +100,11 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).click() } + fun openConversationWithRecipients(recipients: String) { + val matcher = withId(R.id.userName) + withAncestor(R.id.inboxRecyclerView) + withText(recipients) + onView(matcher).scrollTo().click() + } + fun openConversation(conversation: ConversationApiModel) { openConversation(conversation.subject) } @@ -141,12 +151,11 @@ class InboxPage : BasePage(R.id.inboxPage) { fun assertConversationNotStarred(subject: String) { val matcher = allOf( withId(R.id.star), - withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), hasSibling(withId(R.id.userName)), hasSibling(withId(R.id.date)), hasSibling(allOf(withId(R.id.subjectView), withText(subject)))) waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up - onView(matcher).check(doesNotExist()) + onView(matcher).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt index d1a3d9a3ca..dd263c60e8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt @@ -21,6 +21,7 @@ import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithId import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.waitForViewWithId import com.instructure.espresso.page.withId import com.instructure.espresso.scrollTo @@ -29,8 +30,9 @@ import com.instructure.espresso.swipeUp import com.instructure.student.R import org.hamcrest.CoreMatchers import org.hamcrest.Matcher +import java.lang.Thread.sleep -class LeftSideNavigationDrawerPage: BasePage() { +class LeftSideNavigationDrawerPage : BasePage() { private val hamburgerButton by OnViewWithContentDescription(R.string.navigation_drawer_open) @@ -53,6 +55,8 @@ class LeftSideNavigationDrawerPage: BasePage() { private val changeUser by OnViewWithId(R.id.navigationDrawerItem_changeUser) private val logoutButton by OnViewWithId(R.id.navigationDrawerItem_logout) + private val offlineIndicator by OnViewWithId(R.id.navigationDrawerOfflineIndicator, autoAssert = false) + // Sometimes when we navigate back to the dashboard page, there can be several hamburger buttons // in the UI stack. We want to choose the one that is displayed. private val hamburgerButtonMatcher = CoreMatchers.allOf( @@ -61,7 +65,7 @@ class LeftSideNavigationDrawerPage: BasePage() { ) private fun clickMenu(menuId: Int) { - onView(hamburgerButtonMatcher).click() + waitForView(hamburgerButtonMatcher).click() waitForViewWithId(menuId).scrollTo().click() } @@ -71,9 +75,11 @@ class LeftSideNavigationDrawerPage: BasePage() { onViewWithText(android.R.string.yes).click() // It can potentially take a long time for the sign-out to take effect, especially on // slow FTL devices. So let's pause for a bit until we see the canvas logo. - waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check(matches( - ViewMatchers.isDisplayed() - )) + waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check( + matches( + ViewMatchers.isDisplayed() + ) + ) } fun clickChangeUserMenu() { @@ -137,18 +143,29 @@ class LeftSideNavigationDrawerPage: BasePage() { settings.assertDisplayed() - if(CanvasTest.isLandscapeDevice()) onView(withId(R.id.navigationDrawer)).swipeUp() + if(CanvasTest.isLandscapeDevice() || CanvasTest.isLowResDevice()) onView(withId(R.id.navigationDrawer)).swipeUp() changeUser.assertDisplayed() logoutButton.assertDisplayed() - + if (isElementaryStudent) { assertElementaryNavigationBehaviorMenuItems() - } - else { + } else { assertDefaultNavigationBehaviorMenuItems() } } + fun assertOfflineIndicatorDisplayed() { + sleep(1000) //to avoid listview a11y error (content description is missing) + hamburgerButton.click() + offlineIndicator.assertDisplayed() + } + + fun assertOfflineIndicatorNotDisplayed() { + sleep(1000) //to avoid listview a11y error (content description is missing) + hamburgerButton.click() + offlineIndicator.assertNotDisplayed() + } + private fun assertDefaultNavigationBehaviorMenuItems() { if(CanvasTest.isLandscapeDevice()) onView(withId(R.id.navigationDrawer)).swipeDown() files.assertDisplayed() @@ -182,7 +199,7 @@ class LeftSideNavigationDrawerPage: BasePage() { */ private class SetSwitchCompat(val position: Boolean) : ViewAction { override fun getDescription(): String { - val desiredPosition = if(position) "On" else "Off" + val desiredPosition = if (position) "On" else "Off" return "Set SwitchCompat to $desiredPosition" } @@ -192,7 +209,7 @@ class LeftSideNavigationDrawerPage: BasePage() { override fun perform(uiController: UiController?, view: View?) { val switch = view as SwitchCompat - if(switch != null) { + if (switch != null) { switch.isChecked = position } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt index 71dea6a596..430678fc1f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/PageListPage.kt @@ -102,10 +102,10 @@ class PageListPage(val searchable: Searchable) : BasePage(R.id.pageListPage) { fun assertPageNotDisplayed(page: PageApiModel) { // Check for front page - onView(allOf(withId(R.id.homeSubLabel), withText(page.title))).check(DoesNotExistAssertion(10000L)) + onView(allOf(withId(R.id.homeSubLabel), withText(page.title))).check(DoesNotExistAssertion(10)) // Check for regular page - onView(allOf(withId(R.id.title), withText(page.title))).check(DoesNotExistAssertion(10000L)) + onView(allOf(withId(R.id.title), withText(page.title))).check(DoesNotExistAssertion(10)) } fun assertPageListItemCount(expectedCount: Int) { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt index 5de3687694..8f40d0d703 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt @@ -16,15 +16,8 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.TextViewColorAssertion -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.withParent -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo +import com.instructure.espresso.* +import com.instructure.espresso.page.* import com.instructure.student.R class SettingsPage : BasePage(R.id.settingsFragment) { @@ -41,6 +34,7 @@ class SettingsPage : BasePage(R.id.settingsFragment) { private val remoteConfigLabel by OnViewWithId(R.id.remoteConfigParams) private val appThemeTitle by OnViewWithId(R.id.appThemeTitle) private val appThemeStatus by OnViewWithId(R.id.appThemeStatus) + private val offlineContent by OnViewWithId(R.id.offlineSyncSettingsContainer) fun openAboutPage() { aboutLabel.scrollTo().click() @@ -86,4 +80,16 @@ class SettingsPage : BasePage(R.id.settingsFragment) { fun assertAppThemeStatusTextColor(expectedTextColor: String) { appThemeStatus.check(TextViewColorAssertion(expectedTextColor)) } + + fun openOfflineContentPage() { + offlineContent.scrollTo().click() + } + + fun assertOfflineContentDisplayed() { + offlineContent.scrollTo().assertDisplayed() + } + + fun assertOfflineContentNotDisplayed() { + offlineContent.assertNotDisplayed() + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt new file mode 100644 index 0000000000..a7f2db9f3b --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.ui.pages + +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import com.instructure.espresso.* +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onViewWithText +import com.instructure.pandautils.R + +class SyncSettingsPage : BasePage(R.id.syncSettingsPage) { + + private val toolbar by OnViewWithId(R.id.toolbar) + private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch) + private val furtherSettings by OnViewWithId(R.id.furtherSettings) + private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel) + private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch) + + fun clickAutoSyncSwitch() { + autoSyncSwitch.click() + } + + fun clickFrequency() { + syncFrequencyLabel.click() + } + + fun clickDialogOption(stringResId: Int) { + onViewWithText(stringResId).click() + } + + fun clickWifiOnlySwitch() { + wifiOnlySwitch.click() + } + + fun clickTurnOff() { + onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click() + } + + fun assertFurtherSettingsIsDisplayed() { + furtherSettings.assertDisplayed() + } + + fun assertFurtherSettingsNotDisplayed() { + furtherSettings.assertNotDisplayed() + } + + fun assertFrequencyLabelText(expected: Int) { + syncFrequencyLabel.assertHasText(expected) + } + + fun assertWifiOnlySwitchIsChecked() { + wifiOnlySwitch.check(matches(isChecked())) + } + + fun assertWifiOnlySwitchIsNotChecked() { + wifiOnlySwitch.check(matches(isNotChecked())) + } + + fun assertDialogDisplayedWithTitle(title: Int) { + onViewWithText(title).assertDisplayed() + } +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt new file mode 100644 index 0000000000..96c5e6ffea --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.ui.pages.offline + +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText +import com.instructure.pandautils.R + +class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { + + private val toolbar by OnViewWithId(R.id.toolbar) + + //OfflineMethod + fun selectEntireCourseForSync(courseName: String) { + onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(courseName))).click() + } + + //OfflineMethod + fun clickOnSyncButton() { + onView(withId(R.id.syncButton)).click() + confirmSync() + } + + //OfflineMethod + private fun confirmSync() { + waitForView(withText("Sync") + withAncestor(R.id.buttonPanel)).click() + } +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt new file mode 100644 index 0000000000..f49c368035 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/SyncProgressPage.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.ui.pages.offline + +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertVisibility +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.pandautils.R + +class SyncProgressPage : BasePage(R.id.syncProgressPage) { + + private val toolbar by OnViewWithId(R.id.toolbar) + + fun assertDownloadProgressSuccessDetails() { + onView(withId(R.id.downloadProgress)).assertDisplayed() + onView(withId(R.id.errorTitle)).assertVisibility(ViewMatchers.Visibility.GONE) + waitForDownloadSuccess() + } + + fun waitForDownloadStarting() { + waitForView(withId(R.id.downloadProgressText) + containsTextCaseInsensitive("Downloading")).assertDisplayed() + } + + private fun waitForDownloadSuccess() { + waitForView(withId(R.id.downloadProgressText) + containsTextCaseInsensitive("Success! Downloaded")).assertDisplayed() + } + + fun assertCourseSyncedSuccessfully(courseName: String) { + onView(withId(R.id.courseName) + withText(courseName) + withAncestor(R.id.syncProgressPage)).assertDisplayed() + onView(withId(R.id.successIndicator) + withParent(withId(R.id.actionContainer) + hasSibling(withId(R.id.courseName) + withText(courseName)))).assertVisibility(ViewMatchers.Visibility.VISIBLE) + } +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt index 5e969880fb..f6d6478171 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceDetailsRenderTest.kt @@ -15,14 +15,13 @@ */ package com.instructure.student.ui.renderTests -import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.student.espresso.StudentRenderTest -import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsFragment +import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsRepositoryFragment import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsViewState import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceRecordingViewState import com.spotify.mobius.runners.WorkRunner @@ -195,8 +194,8 @@ class ConferenceDetailsRenderTest : StudentRenderTest() { override fun dispose() = Unit override fun post(runnable: Runnable) = Unit } - val route = ConferenceDetailsFragment.makeRoute(canvasContext, Conference()) - val fragment = ConferenceDetailsFragment.newInstance(route)!!.apply { + val route = ConferenceDetailsRepositoryFragment.makeRoute(canvasContext, Conference()) + val fragment = ConferenceDetailsRepositoryFragment.newInstance(route)!!.apply { overrideInitViewState = state loopMod = { it.effectRunner { emptyEffectRunner } } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt index cfd10edeba..794fd1fcef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/ConferenceListRenderTest.kt @@ -16,14 +16,13 @@ package com.instructure.student.ui.renderTests import android.graphics.Color -import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.student.espresso.StudentRenderTest -import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListItemViewState +import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListViewState import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest @@ -133,8 +132,8 @@ class ConferenceListRenderTest : StudentRenderTest() { override fun dispose() = Unit override fun post(runnable: Runnable) = Unit } - val route = ConferenceListFragment.makeRoute(canvasContext) - val fragment = ConferenceListFragment.newInstance(route)!!.apply { + val route = ConferenceListRepositoryFragment.makeRoute(canvasContext) + val fragment = ConferenceListRepositoryFragment.newInstance(route)!!.apply { overrideInitViewState = state loopMod = { it.effectRunner { emptyEffectRunner } } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt index f3a0df8a94..fb226e6cbb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/PairObserverRenderTest.kt @@ -16,16 +16,10 @@ */ package com.instructure.student.ui.renderTests -import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.ScheduleItem -import com.instructure.canvasapi2.utils.DataResult import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.settings.pairobserver.PairObserverModel import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment -import com.instructure.student.mobius.syllabus.SyllabusModel -import com.instructure.student.mobius.syllabus.ui.SyllabusFragment import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt index 68e7c34e9d..ad15b6f54a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionDetailsRenderTest.kt @@ -18,6 +18,7 @@ package com.instructure.student.ui.renderTests import android.content.pm.ActivityInfo import androidx.test.espresso.action.GeneralLocation import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvas.espresso.Stub import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Submission @@ -28,7 +29,7 @@ import com.instructure.espresso.assertVisible import com.instructure.espresso.click import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsModel -import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before @@ -229,6 +230,7 @@ class SubmissionDetailsRenderTest : StudentRenderTest() { } @Test + @Stub fun updatesDrawerHeightOnOrientationChangeToLandscape() { loadPageWithModel( baseModel.copy( @@ -248,6 +250,7 @@ class SubmissionDetailsRenderTest : StudentRenderTest() { } @Test + @Stub fun updatesDrawerHeightOnOrientationChangeToPortrait() { loadPageWithModel( baseModel.copy( @@ -271,8 +274,8 @@ class SubmissionDetailsRenderTest : StudentRenderTest() { override fun dispose() = Unit override fun post(runnable: Runnable) = Unit } - val route = SubmissionDetailsFragment.makeRoute(model.canvasContext, model.assignmentId) - val fragment = SubmissionDetailsFragment.newInstance(route)!!.apply { + val route = SubmissionDetailsRepositoryFragment.makeRoute(model.canvasContext, model.assignmentId) + val fragment = SubmissionDetailsRepositoryFragment.newInstance(route)!!.apply { overrideInitModel = model loopMod = { it.effectRunner { emptyEffectRunner } } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt index 25a5c8ac80..ab45b21fbb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SubmissionFilesRenderTest.kt @@ -84,7 +84,7 @@ class SubmissionFilesRenderTest : StudentRenderTest() { @Test fun displaysFileWithThumbnail() { val data = dataTemplate.copy( - thumbnailUrl = "fake_url" + thumbnailUrl = "https://avatars.githubusercontent.com/u/515326" ) loadPageWithViewState( SubmissionFilesViewState.FileList(listOf(data)) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt index 66b6355a37..8c56f5169f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/renderTests/SyllabusRenderTest.kt @@ -23,7 +23,7 @@ import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.utils.DataResult import com.instructure.student.espresso.StudentRenderTest import com.instructure.student.mobius.syllabus.SyllabusModel -import com.instructure.student.mobius.syllabus.ui.SyllabusFragment +import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment import com.spotify.mobius.runners.WorkRunner import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before @@ -135,8 +135,8 @@ class SyllabusRenderTest : StudentRenderTest() { override fun dispose() = Unit override fun post(runnable: Runnable) = Unit } - val route = SyllabusFragment.makeRoute(model.course?.dataOrNull ?: Course(id = model.courseId)) - val fragment = SyllabusFragment.newInstance(route)!!.apply { + val route = SyllabusRepositoryFragment.makeRoute(model.course?.dataOrNull ?: Course(id = model.courseId)) + val fragment = SyllabusRepositoryFragment.newInstance(route)!!.apply { overrideInitModel = model loopMod = { it.effectRunner { emptyEffectRunner } } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 2285f6c0ce..331d138f10 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -95,9 +95,12 @@ import com.instructure.student.ui.pages.ShareExtensionStatusPage import com.instructure.student.ui.pages.ShareExtensionTargetPage import com.instructure.student.ui.pages.SubmissionDetailsPage import com.instructure.student.ui.pages.SyllabusPage +import com.instructure.student.ui.pages.SyncSettingsPage import com.instructure.student.ui.pages.TextSubmissionUploadPage import com.instructure.student.ui.pages.TodoPage import com.instructure.student.ui.pages.UrlSubmissionUploadPage +import com.instructure.student.ui.pages.offline.ManageOfflineContentPage +import com.instructure.student.ui.pages.offline.SyncProgressPage import dagger.hilt.android.testing.HiltAndroidRule import instructure.rceditor.RCETextEditor import org.hamcrest.Matcher @@ -143,7 +146,7 @@ abstract class StudentTest : CanvasTest() { * Required for auto complete of page objects within tests */ val annotationCommentListPage = AnnotationCommentListPage() - val announcementListPage = AnnouncementListPage() + val announcementListPage = AnnouncementListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) val assignmentDetailsPage = AssignmentDetailsPage() val assignmentListPage = AssignmentListPage(Searchable(R.id.search, R.id.search_src_text)) val bookmarkPage = BookmarkPage() @@ -199,6 +202,9 @@ abstract class StudentTest : CanvasTest() { val importantDatesPage = ImportantDatesPage() val shareExtensionTargetPage = ShareExtensionTargetPage() val shareExtensionStatusPage = ShareExtensionStatusPage() + val syncSettingsPage = SyncSettingsPage() + val manageOfflineContentPage = ManageOfflineContentPage() + val syncProgressPage = SyncProgressPage() // A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger. fun meaninglessSwipe() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt index 6074a719b6..991c0e1778 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTestExtensions.kt @@ -22,6 +22,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Environment +import androidx.fragment.app.FragmentActivity import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId @@ -222,8 +223,8 @@ fun StudentTest.routeTo(route: String, domain: String) { context.startActivity(intent) } -fun StudentTest.routeTo(route: Route) { - RouteMatcher.route(InstrumentationRegistry.getInstrumentation().targetContext, route) +fun StudentTest.routeTo(route: Route, activity: FragmentActivity) { + RouteMatcher.route(activity, route) } fun StudentTest.seedAssignmentSubmission( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt index 6eb9f98b12..80e0d0a305 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/ViewUtils.kt @@ -16,7 +16,11 @@ package com.instructure.student.ui.utils +import android.view.View import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions +import org.hamcrest.Matcher object ViewUtils { @@ -25,4 +29,19 @@ object ViewUtils { Espresso.pressBack() } } + + fun waitForViewToDisappear(viewMatcher: Matcher, timeoutInSeconds: Long) { + val startTime = System.currentTimeMillis() + + while (System.currentTimeMillis() - startTime < (timeoutInSeconds * 1000)) { + try { + onView(viewMatcher) + .check(ViewAssertions.doesNotExist()) + return + } catch (e: AssertionError) { + Thread.sleep(200) + } + } + throw AssertionError("The view has not been displayed within $timeoutInSeconds seconds.") + } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/di/TestDateTimeModule.kt similarity index 54% rename from apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/utils/di/TestDateTimeModule.kt index 7e18e00d08..6a1cdcf3ef 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/TestDateTimeModule.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/di/TestDateTimeModule.kt @@ -1,20 +1,21 @@ /* - * Copyright (C) 2021 - present Instructure, Inc. + * Copyright (C) 2023 - present Instructure, Inc. * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . * */ -package com.instructure.student.ui.utils +package com.instructure.student.ui.utils.di import com.instructure.pandautils.di.DateTimeModule import com.instructure.pandautils.utils.date.DateTimeProvider diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/di/TestOfflineDatabaseProviderModule.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/di/TestOfflineDatabaseProviderModule.kt new file mode 100644 index 0000000000..282f31cecb --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/di/TestOfflineDatabaseProviderModule.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.ui.utils.di + +import android.content.Context +import androidx.room.Room +import com.instructure.pandautils.di.OfflineDatabaseProviderModule +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.offlineDatabaseMigrations +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [OfflineDatabaseProviderModule::class] +) +class TestOfflineDatabaseProviderModule { + + @Provides + @Singleton + fun provideOfflineDatabaseProvider(@ApplicationContext context: Context): DatabaseProvider { + return FakeOfflineDatabaseProvider(context) + } +} + +class FakeOfflineDatabaseProvider(private val context: Context) : DatabaseProvider { + + private val dbMap = mutableMapOf() + + override fun getDatabase(userId: Long?): OfflineDatabase { + if (userId == null) return Room.databaseBuilder(context, OfflineDatabase::class.java, "test-offline-db") + .addMigrations(*offlineDatabaseMigrations) + .build() + + return dbMap.getOrPut(userId) { + Room.databaseBuilder(context, OfflineDatabase::class.java, "offline-db-$userId") + .addMigrations(*offlineDatabaseMigrations) + .build() + } + } + + override fun clearDatabase(userId: Long) {} +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt index cdf7fd15fd..988a81b30b 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/CallbackActivity.kt @@ -211,6 +211,7 @@ abstract class CallbackActivity : ParentActivity(), OnUnreadCountInvalidated, No } ApiType.CACHE -> if (!APIHelper.hasNetworkConnection()) ApiPrefs.user = user ApiType.UNKNOWN -> {} + else -> {} } return false } diff --git a/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt index bee62d8765..2a1e341052 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/CandroidPSPDFActivity.kt @@ -28,10 +28,12 @@ import android.view.MenuItem import androidx.annotation.ColorInt import androidx.core.text.TextUtilsCompat import com.instructure.annotations.CanvasPdfMenuGrouping +import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog import com.instructure.pandautils.analytics.SCREEN_VIEW_PSPDFKIT import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.pandautils.utils.ViewStyler import com.instructure.student.R import com.instructure.student.features.shareextension.StudentShareExtensionActivity @@ -45,9 +47,12 @@ import com.pspdfkit.ui.toolbar.AnnotationCreationToolbar import com.pspdfkit.ui.toolbar.ContextualToolbar import com.pspdfkit.ui.toolbar.ContextualToolbarMenuItem import com.pspdfkit.ui.toolbar.ToolbarCoordinatorLayout +import dagger.hilt.android.AndroidEntryPoint import java.util.* +import javax.inject.Inject @ScreenView(SCREEN_VIEW_PSPDFKIT) +@AndroidEntryPoint class CandroidPSPDFActivity : PdfActivity(), ToolbarCoordinatorLayout.OnContextualToolbarLifecycleListener { override fun onDisplayContextualToolbar(p0: ContextualToolbar<*>) {} override fun onRemoveContextualToolbar(p0: ContextualToolbar<*>) {} @@ -56,6 +61,9 @@ class CandroidPSPDFActivity : PdfActivity(), ToolbarCoordinatorLayout.OnContextu private val submissionTarget by lazy { intent?.extras?.getParcelable(Const.SUBMISSION_TARGET) } + @Inject + lateinit var networkStateProvider: NetworkStateProvider + override fun onPrepareContextualToolbar(toolbar: ContextualToolbar<*>) { if(toolbar is AnnotationCreationToolbar) { toolbar.setMenuItemGroupingRule(CanvasPdfMenuGrouping(this)) @@ -122,10 +130,14 @@ class CandroidPSPDFActivity : PdfActivity(), ToolbarCoordinatorLayout.OnContextu private fun uploadDocumentToCanvas() { if (document != null) { - DocumentSharingManager.shareDocument( + if (networkStateProvider.isOnline()) { + DocumentSharingManager.shareDocument( CandroidDocumentSharingController(this, submissionTarget), document!!, SharingOptions(PdfProcessorTask.AnnotationProcessingMode.FLATTEN)) + } else { + NoInternetConnectionDialog.show(supportFragmentManager) + } } } diff --git a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt index 1bef26bcb6..f73493b59f 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/InterwebsToApplication.kt @@ -126,6 +126,7 @@ class InterwebsToApplication : AppCompatActivity() { val intent = if(tokenResponse.realUser != null && tokenResponse.user != null) { // We need to set the masquerade request to the user (masqueradee), the real user it the admin user currently masquerading ApiPrefs.isMasqueradingFromQRCode = true + ApiPrefs.masqueradeId = tokenResponse.user!!.id NavigationActivity.createIntent(this@InterwebsToApplication, tokenResponse.user!!.id) } else { // Log the analytics - only for real logins, not masquerading diff --git a/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt index cd4b5e77a4..7e06bc1cf6 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/LoginLandingPageActivity.kt @@ -19,17 +19,29 @@ package com.instructure.student.activity import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.models.AccountDomain +import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.loginapi.login.activities.BaseLoginLandingPageActivity +import com.instructure.loginapi.login.model.SignedInUser import com.instructure.pandautils.analytics.SCREEN_VIEW_LOGIN_LANDING import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.student.R import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import javax.inject.Inject @ScreenView(SCREEN_VIEW_LOGIN_LANDING) @AndroidEntryPoint class LoginLandingPageActivity : BaseLoginLandingPageActivity() { + @Inject + lateinit var databaseProvider: DatabaseProvider + override fun beginFindSchoolFlow(): Intent { return FindSchoolActivity.createIntent(this) } @@ -54,6 +66,17 @@ class LoginLandingPageActivity : BaseLoginLandingPageActivity() { override fun loginWithQRIntent(): Intent = Intent(this, StudentLoginWithQRActivity::class.java) + override fun removePreviousUser(user: SignedInUser) { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + val userId = user.user.id + File(ContextKeeper.appContext.filesDir, userId.toString()).deleteRecursively() + databaseProvider.clearDatabase(userId) + super.removePreviousUser(user) + } + } + } + companion object { fun createIntent(context: Context): Intent { return Intent(context, LoginLandingPageActivity::class.java) diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index f665f6802c..3521f438d0 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -49,7 +49,6 @@ import androidx.lifecycle.lifecycleScope import com.airbnb.lottie.LottieAnimationView import com.google.android.material.bottomnavigation.BottomNavigationView import com.instructure.canvasapi2.CanvasRestAdapter -import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.managers.UserManager import com.instructure.canvasapi2.models.* @@ -72,6 +71,8 @@ import com.instructure.pandautils.features.themeselector.ThemeSelectorBottomShee import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.models.PushNotification import com.instructure.pandautils.receivers.PushExternalReceiver +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.room.offline.OfflineDatabase import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.pandautils.update.UpdateManager import com.instructure.pandautils.utils.* @@ -84,6 +85,9 @@ import com.instructure.student.databinding.LoadingCanvasViewBinding import com.instructure.student.databinding.NavigationDrawerBinding import com.instructure.student.dialog.BookmarkCreationDialog import com.instructure.student.events.* +import com.instructure.student.features.files.list.FileListFragment +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment +import com.instructure.student.features.navigation.NavigationRepository import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.fragment.* import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler @@ -131,9 +135,21 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. @Inject lateinit var updateManager: UpdateManager + @Inject + lateinit var networkStateProvider: NetworkStateProvider + + @Inject + lateinit var databaseProvider: DatabaseProvider + @Inject lateinit var featureFlagProvider: FeatureFlagProvider + @Inject + lateinit var repository: NavigationRepository + + @Inject + lateinit var offlineDatabase: OfflineDatabase + private var routeJob: WeaveJob? = null private var debounceJob: Job? = null private var drawerItemSelectedJob: Job? = null @@ -180,13 +196,21 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. }, route) } R.id.navigationDrawerItem_changeUser -> { - StudentLogoutTask(if (ApiPrefs.isStudentView) LogoutTask.Type.LOGOUT else LogoutTask.Type.SWITCH_USERS, typefaceBehavior = typefaceBehavior).execute() + StudentLogoutTask( + if (ApiPrefs.isStudentView) LogoutTask.Type.LOGOUT else LogoutTask.Type.SWITCH_USERS, + typefaceBehavior = typefaceBehavior, + databaseProvider = databaseProvider + ).execute() } R.id.navigationDrawerItem_logout -> { AlertDialog.Builder(this@NavigationActivity) .setTitle(R.string.logout_warning) .setPositiveButton(android.R.string.yes) { _, _ -> - StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior).execute() + StudentLogoutTask( + LogoutTask.Type.LOGOUT, + typefaceBehavior = typefaceBehavior, + databaseProvider = databaseProvider + ).execute() } .setNegativeButton(android.R.string.no, null) .create() @@ -214,6 +238,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. from external sources. */ val visible = isBottomNavFragment(it) || supportFragmentManager.backStackEntryCount <= 1 binding.bottomBar.setVisible(visible) + binding.divider.setVisible(visible) } } @@ -245,6 +270,8 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + RouteMatcher.offlineDb = offlineDatabase + RouteMatcher.networkStateProvider = networkStateProvider navigationDrawerBinding = NavigationDrawerBinding.bind(binding.root) canvasLoadingBinding = LoadingCanvasViewBinding.bind(binding.root) setContentView(binding.root) @@ -284,6 +311,24 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } requestNotificationsPermission() + + networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> + setOfflineState(!isOnline) + handleTokenCheck(isOnline) + } + } + + private fun handleTokenCheck(online: Boolean?) { + val checkToken = ApiPrefs.checkTokenAfterOfflineLogin + if (checkToken && online == true) { + ApiPrefs.checkTokenAfterOfflineLogin = false + lifecycleScope.launch { + val isTokenValid = repository.isTokenValid() + if (!isTokenValid) { + StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider).execute() + } + } + } } private fun loadFeatureFlags() { @@ -306,6 +351,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. currentFragment?.let { val visible = isBottomNavFragment(it) || supportFragmentManager.backStackEntryCount <= 1 binding.bottomBar.setVisible(visible) + binding.divider.setVisible(visible) } } @@ -338,7 +384,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. if (ApiPrefs.user == null ) { // Hard case to repro but it's possible for a user to force exit the app before we finish saving the user but they will still launch into the app // If that happens, log out - StudentLogoutTask(LogoutTask.Type.LOGOUT).execute() + StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute() } setupBottomNavigation() @@ -468,12 +514,12 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. override fun attachNavigationDrawer(fragment: F, toolbar: Toolbar) where F : Fragment, F : FragmentInteractions { //Navigation items - navigationDrawerBinding.navigationDrawerItemFiles.setOnClickListener(mNavigationDrawerItemClickListener) - navigationDrawerBinding.navigationDrawerItemGauge.setOnClickListener(mNavigationDrawerItemClickListener) - navigationDrawerBinding.navigationDrawerItemStudio.setOnClickListener(mNavigationDrawerItemClickListener) - navigationDrawerBinding.navigationDrawerItemBookmarks.setOnClickListener(mNavigationDrawerItemClickListener) + navigationDrawerBinding.navigationDrawerItemFiles.onClickWithRequireNetwork(mNavigationDrawerItemClickListener) + navigationDrawerBinding.navigationDrawerItemGauge.onClickWithRequireNetwork(mNavigationDrawerItemClickListener) + navigationDrawerBinding.navigationDrawerItemStudio.onClickWithRequireNetwork(mNavigationDrawerItemClickListener) + navigationDrawerBinding.navigationDrawerItemBookmarks.onClickWithRequireNetwork(mNavigationDrawerItemClickListener) navigationDrawerBinding.navigationDrawerItemChangeUser.setOnClickListener(mNavigationDrawerItemClickListener) - navigationDrawerBinding.navigationDrawerItemHelp.setOnClickListener(mNavigationDrawerItemClickListener) + navigationDrawerBinding.navigationDrawerItemHelp.onClickWithRequireNetwork(mNavigationDrawerItemClickListener) navigationDrawerBinding.navigationDrawerItemLogout.setOnClickListener(mNavigationDrawerItemClickListener) navigationDrawerBinding.navigationDrawerSettings.setOnClickListener(mNavigationDrawerItemClickListener) navigationDrawerBinding.navigationDrawerItemStartMasquerading.setOnClickListener(mNavigationDrawerItemClickListener) @@ -570,6 +616,27 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. ViewStyler.themeSwitch(this@NavigationActivity, navigationDrawerColorOverlaySwitch, ThemePrefs.brandColor) } + private fun setOfflineState(isOffline: Boolean) { + binding.offlineIndicator.root.setVisible(isOffline) + with(navigationDrawerBinding) { + navigationDrawerOfflineIndicator.setVisible(isOffline) + navigationDrawerItemStudio.alpha = if (isOffline) 0.5f else 1f + navigationDrawerItemGauge.alpha = if (isOffline) 0.5f else 1f + navigationDrawerItemHelp.alpha = if (isOffline) 0.5f else 1f + navigationDrawerItemBookmarks.alpha = if (isOffline) 0.5f else 1f + navigationDrawerItemFiles.alpha = if (isOffline) 0.5f else 1f + navigationDrawerItemColorOverlay.alpha = if (isOffline) 0.5f else 1f + navigationDrawerColorOverlaySwitch.isEnabled = !isOffline + } + + with(binding.bottomBar.menu) { + findItem(R.id.bottomNavigationCalendar).isEnabled = !isOffline + findItem(R.id.bottomNavigationToDo).isEnabled = !isOffline + findItem(R.id.bottomNavigationNotifications).isEnabled = !isOffline + findItem(R.id.bottomNavigationInbox).isEnabled = !isOffline + } + } + override fun onStartMasquerading(domain: String, userId: Long) { MasqueradeHelper.startMasquerading(userId, domain, NavigationActivity::class.java) } @@ -759,7 +826,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val contextType = route.getContextType() when (contextType) { CanvasContext.Type.COURSE -> { - route.canvasContext = awaitApi { CourseManager.getCourse(contextId, it, false) } + route.canvasContext = repository.getCourse(contextId, false) if(route.canvasContext == null) showMessage(getString(R.string.could_not_route_course)) } CanvasContext.Type.GROUP -> { @@ -784,7 +851,7 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. val contextType = route.getContextType() when (contextType) { CanvasContext.Type.COURSE -> { - route.canvasContext = awaitApi { CourseManager.getCourse(contextId, it, false) } + route.canvasContext = repository.getCourse(contextId, false) if(route.canvasContext == null) showMessage(getString(R.string.could_not_route_course)) } CanvasContext.Type.GROUP -> { diff --git a/apps/student/src/main/java/com/instructure/student/activity/NotificationWidgetRouter.kt b/apps/student/src/main/java/com/instructure/student/activity/NotificationWidgetRouter.kt index 3d2b6f2752..d377b5a676 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NotificationWidgetRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NotificationWidgetRouter.kt @@ -31,7 +31,7 @@ class NotificationWidgetRouter : ParentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) handleIntent() - streamItem?.let { addFragmentForStreamItem(it, context, true) } + streamItem?.let { addFragmentForStreamItem(it, this, true) } finish() } diff --git a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt index 23b3613abe..78a7509f68 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt @@ -23,16 +23,30 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS import com.instructure.pandautils.analytics.ScreenView +import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.pandautils.utils.setVisible import com.instructure.student.R +import com.instructure.student.databinding.ActivitySettingsBinding import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @ScreenView(SCREEN_VIEW_SETTINGS) @AndroidEntryPoint class SettingsActivity : AppCompatActivity(){ + @Inject + lateinit var networkStateProvider: NetworkStateProvider + + private val binding by viewBinding(ActivitySettingsBinding::inflate) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) + setContentView(binding.root) + + networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> + binding.offlineIndicator.root.setVisible(!isOnline) + } } private val currentFragment: Fragment? get() = supportFragmentManager.fragments.last() diff --git a/apps/student/src/main/java/com/instructure/student/adapter/CourseBrowserAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/CourseBrowserAdapter.kt index 0eaa028e43..a0723d4818 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/CourseBrowserAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/CourseBrowserAdapter.kt @@ -26,6 +26,7 @@ import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.pandautils.utils.textAndIconColor import com.instructure.student.R import com.instructure.student.databinding.AdapterCourseBrowserBinding @@ -118,7 +119,9 @@ class CourseBrowserWebViewHolder(view: View, val color: Int) : RecyclerView.View binding.unsupportedLabel.text = tab.label binding.unsupportedIcon.setImageDrawable(drawable) binding.unsupportedSubLabel.setText(R.string.opensInWebView) - itemView.setOnClickListener { callback(tab) } + itemView.isEnabled = tab.enabled + itemView.alpha = if (tab.enabled) 1f else 0.5f + itemView.onClickWithRequireNetwork { callback(tab) } } companion object { @@ -170,8 +173,16 @@ class CourseBrowserViewHolder(view: View, val color: Int) : RecyclerView.ViewHol val binding = AdapterCourseBrowserBinding.bind(itemView) binding.label.text = tab.label binding.icon.setImageDrawable(drawable) - itemView.setOnClickListener { - callback(tab) + itemView.isEnabled = tab.enabled + itemView.alpha = if (tab.enabled) 1f else 0.5f + if (tab.type == Tab.TYPE_EXTERNAL) { + itemView.onClickWithRequireNetwork { + callback(tab) + } + } else { + itemView.setOnClickListener { + callback(tab) + } } } diff --git a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt index 1dd47f1cf6..725f9456d3 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt @@ -25,7 +25,9 @@ import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.canvasapi2.utils.weave.* import com.instructure.pandarecycler.util.GroupSortedList +import com.instructure.pandautils.features.dashboard.DashboardCourseItem import com.instructure.pandautils.utils.ColorApiHelper +import com.instructure.student.features.dashboard.DashboardRepository import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.holders.* import com.instructure.student.interfaces.CourseAdapterToFragmentCallback @@ -34,7 +36,8 @@ import java.util.* class DashboardRecyclerAdapter( context: Activity, - private val mAdapterToFragmentCallback: CourseAdapterToFragmentCallback + private val mAdapterToFragmentCallback: CourseAdapterToFragmentCallback, + private val repository: DashboardRepository ) : ExpandableRecyclerAdapter( context, ItemType::class.java, @@ -51,6 +54,8 @@ class DashboardRecyclerAdapter( private var mApiCalls: WeaveJob? = null private var mCourseMap = mapOf() + private var isOfflineEnabled = false + init { isExpandedByDefault = true loadData() @@ -65,7 +70,7 @@ class DashboardRecyclerAdapter( override fun onBindChildHolder(holder: RecyclerView.ViewHolder, header: ItemType, item: Any) { when { - holder is CourseViewHolder && item is Course -> holder.bind(item, mAdapterToFragmentCallback) + holder is CourseViewHolder && item is DashboardCourseItem -> holder.bind(item, isOfflineEnabled, mAdapterToFragmentCallback) holder is GroupViewHolder && item is Group -> holder.bind(item, mCourseMap, mAdapterToFragmentCallback) } } @@ -77,7 +82,7 @@ class DashboardRecyclerAdapter( override fun createItemCallback(): GroupSortedList.ItemComparatorCallback { return object : GroupSortedList.ItemComparatorCallback { override fun compare(group: ItemType, o1: Any, o2: Any) = when { - o1 is Course && o2 is Course -> -1 // Don't sort courses, the api returns in the users order + o1 is DashboardCourseItem && o2 is DashboardCourseItem -> -1 // Don't sort courses, the api returns in the users order o1 is Group && o2 is Group -> o1.compareTo(o2) else -> -1 } @@ -85,19 +90,19 @@ class DashboardRecyclerAdapter( override fun areContentsTheSame(oldItem: Any, newItem: Any) = false override fun areItemsTheSame(item1: Any, item2: Any) = when { - item1 is Course && item2 is Course -> item1.contextId.hashCode() == item2.contextId.hashCode() + item1 is DashboardCourseItem && item2 is DashboardCourseItem -> item1.course.contextId.hashCode() == item2.course.contextId.hashCode() item1 is Group && item2 is Group -> item1.contextId.hashCode() == item2.contextId.hashCode() else -> false } override fun getUniqueItemId(item: Any) = when (item) { - is Course -> item.contextId.hashCode().toLong() + is DashboardCourseItem -> item.course.contextId.hashCode().toLong() is Group -> item.contextId.hashCode().toLong() else -> -1L } override fun getChildType(group: ItemType, item: Any) = when (item) { - is Course -> ItemType.COURSE.ordinal + is DashboardCourseItem -> ItemType.COURSE.ordinal is Group -> ItemType.GROUP.ordinal else -> -1 } @@ -118,17 +123,19 @@ class DashboardRecyclerAdapter( override fun loadData() { mApiCalls?.cancel() mApiCalls = tryWeave { - if (isRefresh) { + if (isRefresh && repository.isOnline()) { ColorApiHelper.awaitSync() FlutterComm.sendUpdatedTheme() } - val (rawCourses, groups) = awaitApis, List>( - { CourseManager.getCourses(isRefresh, it) }, - { GroupManager.getAllGroups(it, isRefresh) } - ) - val dashboardCards = awaitApi> { CourseManager.getDashboardCourses(isRefresh, it) } - mCourseMap = rawCourses.associateBy { it.id } + isOfflineEnabled = repository.isOfflineEnabled() + + val courses = repository.getCourses(isRefresh) + val groups = repository.getGroups(isRefresh) + val dashboardCards = repository.getDashboardCourses(isRefresh) + val syncedCourseIds = repository.getSyncedCourseIds() + + mCourseMap = courses.associateBy { it.id } // Map not null is needed because the dashboard api can return unpublished courses val visibleCourses = dashboardCards.map { createCourseFromDashboardCard(it, mCourseMap) } @@ -140,7 +147,11 @@ class DashboardRecyclerAdapter( val visibleGroups = if (isAnyGroupFavorited) allActiveGroups.filter { it.isFavorite } else allActiveGroups // Add courses - addOrUpdateAllItems(ItemType.COURSE_HEADER, visibleCourses) + val isOnline = repository.isOnline() + val courseItems = visibleCourses.map { + DashboardCourseItem(it, syncedCourseIds.contains(it.id), isOnline || mCourseMap.containsKey(it.id)) + } + addOrUpdateAllItems(ItemType.COURSE_HEADER, courseItems) // Add groups addOrUpdateAllItems(ItemType.GROUP_HEADER, visibleGroups) diff --git a/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt deleted file mode 100644 index ca0bc80e2f..0000000000 --- a/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.instructure.student.adapter - -import android.app.ProgressDialog -import android.content.Context -import android.graphics.Color -import android.graphics.PorterDuff -import android.graphics.drawable.ColorDrawable -import android.os.CountDownTimer -import android.view.View -import android.view.WindowManager -import android.widget.ProgressBar -import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.StatusCallback -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.ModuleManager -import com.instructure.canvasapi2.managers.TabManager -import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.ApiType -import com.instructure.canvasapi2.utils.DateHelper -import com.instructure.canvasapi2.utils.LinkHeaders -import com.instructure.canvasapi2.utils.weave.awaitApi -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave -import com.instructure.pandarecycler.interfaces.ViewHolderHeaderClicked -import com.instructure.pandarecycler.util.GroupSortedList -import com.instructure.pandarecycler.util.Types -import com.instructure.pandautils.utils.Utils -import com.instructure.pandautils.utils.orDefault -import com.instructure.pandautils.utils.textAndIconColor -import com.instructure.student.R -import com.instructure.student.holders.ModuleEmptyViewHolder -import com.instructure.student.holders.ModuleHeaderViewHolder -import com.instructure.student.holders.ModuleSubHeaderViewHolder -import com.instructure.student.holders.ModuleViewHolder -import com.instructure.student.interfaces.ModuleAdapterToFragmentCallback -import com.instructure.student.util.CollapsedModulesStore -import com.instructure.student.util.ModuleUtility -import kotlinx.coroutines.Job -import retrofit2.Call -import retrofit2.Response -import java.util.* - -open class ModuleListRecyclerAdapter( - private val courseContext: CanvasContext, - context: Context, - private var shouldExhaustPagination: Boolean, - private val adapterToFragmentCallback: ModuleAdapterToFragmentCallback? -) : ExpandableRecyclerAdapter(context, ModuleObject::class.java, ModuleItem::class.java) { - - private val mModuleItemCallbacks = HashMap() - private var mModuleObjectCallback: StatusCallback>? = null - private var getInitialDataJob: Job? = null - - private var courseSettings: CourseSettings? = null - - /* For testing purposes only */ - protected constructor(context: Context) : this(CanvasContext.defaultCanvasContext(), context, false,null) // Callback not needed for testing, cast to null - - init { - viewHolderHeaderClicked = object : ViewHolderHeaderClicked { - override fun viewClicked(view: View?, moduleObject: ModuleObject) { - val moduleItemsCallback = getModuleItemsCallback(moduleObject, false) - if (!moduleItemsCallback.isFromNetwork && !isGroupExpanded(moduleObject)) { - ModuleManager.getFirstPageModuleItems(courseContext, moduleObject.id, moduleItemsCallback, true) - } else { - CollapsedModulesStore.markModuleCollapsed(courseContext, moduleObject.id, true) - expandCollapseGroup(moduleObject) - } - } - - } - isExpandedByDefault = false - isDisplayEmptyCell = true - if (adapterToFragmentCallback != null) loadData() // Callback is null when testing - } - - override fun createViewHolder(v: View, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - Types.TYPE_HEADER -> ModuleHeaderViewHolder(v) - Types.TYPE_SUB_HEADER -> ModuleSubHeaderViewHolder(v) - Types.TYPE_EMPTY_CELL -> ModuleEmptyViewHolder(v) - else -> ModuleViewHolder(v) - } - } - - override fun onBindChildHolder(holder: RecyclerView.ViewHolder, moduleObject: ModuleObject, moduleItem: ModuleItem) { - if (holder is ModuleSubHeaderViewHolder) { - val groupItemCount = getGroupItemCount(moduleObject) - val itemPosition = storedIndexOfItem(moduleObject, moduleItem) - holder.bind(moduleItem, itemPosition == 0, itemPosition == groupItemCount - 1) - } else { - val courseColor = courseContext.textAndIconColor - val groupItemCount = getGroupItemCount(moduleObject) - val itemPosition = storedIndexOfItem(moduleObject, moduleItem) - - (holder as ModuleViewHolder).bind( - moduleObject, moduleItem, context, adapterToFragmentCallback, courseColor, itemPosition == 0, - itemPosition == groupItemCount - 1, courseSettings?.restrictQuantitativeData.orDefault() - ) - } - } - - override fun onBindHeaderHolder(holder: RecyclerView.ViewHolder, moduleObject: ModuleObject, isExpanded: Boolean) { - (holder as ModuleHeaderViewHolder).bind(moduleObject, context, viewHolderHeaderClicked, isExpanded) - } - - override fun onBindEmptyHolder(holder: RecyclerView.ViewHolder, moduleObject: ModuleObject) { - val moduleEmptyViewHolder = holder as ModuleEmptyViewHolder - // Keep displaying No connection as long as the result is not from network - // Doing so will cause the user to toggle the expand to refresh the list, if they had expanded a module while offline - if (mModuleItemCallbacks.containsKey(moduleObject.id) && mModuleItemCallbacks[moduleObject.id]!!.isFromNetwork) { - moduleEmptyViewHolder.bind(getPrerequisiteString(moduleObject)) - } else { - moduleEmptyViewHolder.bind(context.getString(R.string.noConnection)) - } - } - - override fun itemLayoutResId(viewType: Int): Int { - return when (viewType) { - Types.TYPE_HEADER -> ModuleHeaderViewHolder.HOLDER_RES_ID - Types.TYPE_SUB_HEADER -> ModuleSubHeaderViewHolder.HOLDER_RES_ID - Types.TYPE_EMPTY_CELL -> ModuleEmptyViewHolder.HOLDER_RES_ID - else -> ModuleViewHolder.HOLDER_RES_ID - } - } - - override fun refresh() { - shouldExhaustPagination = false - mModuleItemCallbacks.clear() - getInitialDataJob?.cancel() - collapseAll() - super.refresh() - } - - override fun cancel() { - mModuleItemCallbacks.values.forEach { it.cancel() } - mModuleObjectCallback?.cancel() - getInitialDataJob?.cancel() - } - - override fun contextReady() { - - } - - // region Expandable Callbacks - override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback { - return object : GroupSortedList.GroupComparatorCallback { - override fun compare(o1: ModuleObject, o2: ModuleObject): Int = o1.position - o2.position - override fun areContentsTheSame(oldGroup: ModuleObject, newGroup: ModuleObject): Boolean { - val isNewLocked = ModuleUtility.isGroupLocked(newGroup) - val isOldLocked = ModuleUtility.isGroupLocked(oldGroup) - return oldGroup.name == newGroup.name && isNewLocked == isOldLocked - } - - override fun areItemsTheSame(group1: ModuleObject, group2: ModuleObject): Boolean = group1.id == group2.id - override fun getGroupType(group: ModuleObject): Int = Types.TYPE_HEADER - override fun getUniqueGroupId(group: ModuleObject): Long = group.id - } - } - - override fun createItemCallback(): GroupSortedList.ItemComparatorCallback { - return object : GroupSortedList.ItemComparatorCallback { - override fun compare(group: ModuleObject, o1: ModuleItem, o2: ModuleItem): Int = o1.position - o2.position - override fun areContentsTheSame(oldItem: ModuleItem, newItem: ModuleItem): Boolean = oldItem.title == newItem.title - override fun areItemsTheSame(item1: ModuleItem, item2: ModuleItem): Boolean = item1.id == item2.id - - override fun getChildType(group: ModuleObject, item: ModuleItem): Int { - return if (item.type == ModuleItem.Type.SubHeader.toString()) { - Types.TYPE_SUB_HEADER - } else Types.TYPE_ITEM - } - - override fun getUniqueItemId(item: ModuleItem): Long = item.id - } - } - // endregion - - - private fun createProgressDialog(context: Context): ProgressDialog { - val dialog = ProgressDialog(context) - try { - dialog.show() - } catch (e: WindowManager.BadTokenException) { - } - - dialog.setCancelable(false) - - dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - dialog.setContentView(R.layout.progress_dialog) - val currentColor = courseContext.textAndIconColor - - (dialog.findViewById(R.id.progressBar) as ProgressBar).indeterminateDrawable.setColorFilter(currentColor, PorterDuff.Mode.SRC_ATOP) - return dialog - } - - fun updateWithoutResettingViews(moduleObject: ModuleObject) { - mModuleItemCallbacks.clear() - mModuleObjectCallback!!.cancel() - - ModuleManager.getFirstPageModuleItems(courseContext, moduleObject.id, getModuleItemsCallback(moduleObject, false), true) - } - - fun updateMasteryPathItems() { - - val dialog = createProgressDialog(context) - dialog.show() - // Show for 3 seconds and then refresh the list - // This 3 seconds is to allow the Canvas database to update so we can pull the module info down - object : CountDownTimer(3000, 1000) { - - override fun onTick(millisUntilFinished: Long) {} - - override fun onFinish() { - dialog.cancel() - refresh() - } - }.start() - } - - private fun getModuleItemsCallback(moduleObject: ModuleObject, isNotifyGroupChange: Boolean): ModuleItemCallback { - if (mModuleItemCallbacks.containsKey(moduleObject.id)) { - return mModuleItemCallbacks[moduleObject.id]!! - } else { - val moduleItemCallback = object : ModuleItemCallback(moduleObject) { - - private fun checkMasteryPaths(position: Int, item: ModuleItem): Int { - var position = position - if (item.masteryPaths != null && item.masteryPaths!!.isLocked) { - // Add another module item that says it's locked - val masteryPathsLocked = ModuleItem( - // Set an id so that if there is more than one path we'll display all of them. otherwise addOrUpdateItem will overwrite it - id = UUID.randomUUID().leastSignificantBits, - title = String.format(Locale.getDefault(), context.getString(R.string.locked_mastery_paths), item.title), - type = ModuleItem.Type.Locked.toString(), - completionRequirement = null, - position = position++ - ) - addOrUpdateItem(this.moduleObject, masteryPathsLocked) - } else if (item.masteryPaths != null && !item.masteryPaths!!.isLocked && item.masteryPaths!!.selectedSetId == 0L) { - // Add another module item that says select to choose assignment group - // We only want to do this when we have a mastery paths object, it's unlocked, and the user hasn't already selected a set - val masteryPathsSelect = ModuleItem( - // Set an id so that if there is more than one path we'll display all of them. otherwise addOrUpdateItem will overwrite it - id = UUID.randomUUID().leastSignificantBits, - title = context.getString(R.string.choose_assignment_group), - type = ModuleItem.Type.ChooseAssignmentGroup.toString(), - completionRequirement = null, - position = position++ - ) - - // Sort the mastery paths by position - item.masteryPaths!!.assignmentSets!!.sortBy { it?.position } - masteryPathsSelect.masteryPathsItemId = item.id - masteryPathsSelect.masteryPaths = item.masteryPaths - addOrUpdateItem(this.moduleObject, masteryPathsSelect) - notifyDataSetChanged() - } - return position - } - - override fun onResponse(response: Response>, linkHeaders: LinkHeaders, type: ApiType) { - val moduleItems = response.body() - if (type === ApiType.API) { - var position = if (moduleItems!!.isNotEmpty() && moduleItems[0] != null) moduleItems[0].position - 1 else 0 - for (item in moduleItems) { - item.position = position++ - addOrUpdateItem(this.moduleObject, item) - position = checkMasteryPaths(position, item) - } - - val nextItemsURL = linkHeaders.nextUrl - if (nextItemsURL != null) { - ModuleManager.getNextPageModuleItems(nextItemsURL, this, true) - } - - this.isFromNetwork = true - expandGroup(this.moduleObject, isNotifyGroupChange) - } else if (type === ApiType.CACHE) { - var position = if (moduleItems!!.isNotEmpty() && moduleItems[0] != null) moduleItems[0].position - 1 else 0 - for (item in moduleItems) { - item.position = position++ - addOrUpdateItem(this.moduleObject, item) - } - - val nextItemsURL = linkHeaders.nextUrl - if (nextItemsURL != null) { - ModuleManager.getNextPageModuleItems(nextItemsURL, this, true) - } - - // Wait for the network to expand when there are no items - if (moduleItems.isNotEmpty()) { - expandGroup(this.moduleObject, isNotifyGroupChange) - } - } - CollapsedModulesStore.markModuleCollapsed(courseContext, moduleObject.id, false) - } - - override fun onFail(call: Call>?, error: Throwable, response: Response<*>?) { - - // Only expand if there was no cache result and no network. No connection empty cell will be displayed - if (response != null - && response.code() == 504 - && APIHelper.isCachedResponse(response) - && context != null - && !Utils.isNetworkAvailable(context)) { - expandGroup(this.moduleObject, isNotifyGroupChange) - } - } - } - - mModuleItemCallbacks[moduleObject.id] = moduleItemCallback - return moduleItemCallback - } - } - - // region Pagination - override val isPaginated get() = true - - override fun setupCallbacks() { - mModuleObjectCallback = object : StatusCallback>() { - - override fun onResponse(response: Response>, linkHeaders: LinkHeaders, type: ApiType) { - val moduleObjects = response.body() - setNextUrl(linkHeaders.nextUrl) - val collapsedItems = CollapsedModulesStore.getCollapsedModuleIds(courseContext) - moduleObjects?.toTypedArray()?.forEach { - addOrUpdateGroup(it) - if (!collapsedItems.contains(it.id)) { - ModuleManager.getFirstPageModuleItems(courseContext, it.id, getModuleItemsCallback(it, true), true) - } - } - if(!shouldExhaustPagination || !this.moreCallsExist()) { - // If we should exhaust pagination wait until we are done exhausting pagination - adapterToFragmentCallback?.onRefreshFinished() - } - } - - override fun onFinished(type: ApiType) { - this@ModuleListRecyclerAdapter.onCallbackFinished(type) - } - } - } - - override fun loadFirstPage() { - getInitialDataJob = tryWeave { - val tabs = awaitApi { TabManager.getTabs(courseContext, it, isRefresh) } - .filter { !(it.isExternal && it.isHidden) } - - courseSettings = CourseManager.getCourseSettingsAsync(courseContext.id, isRefresh).await().dataOrNull - - // We only want to show modules if its a course nav option OR set to as the homepage - if (tabs.find { it.tabId == "modules" } != null || (courseContext as Course).homePage?.apiString == "modules") { - if (shouldExhaustPagination) { - ModuleManager.getAllModuleObjets(courseContext, mModuleObjectCallback!!, true) - } else { - ModuleManager.getFirstPageModuleObjects(courseContext, mModuleObjectCallback!!, true) - } - } else { - adapterToFragmentCallback?.onRefreshFinished(true) - } - } catch { - adapterToFragmentCallback?.onRefreshFinished(true) - } - } - - override fun loadNextPage(nextURL: String) { - ModuleManager.getNextPageModuleObjects(nextURL, mModuleObjectCallback!!, true) - } - - // endregion - - // region Module binder Helpers - private fun isSequentiallyEnabled(moduleObject: ModuleObject, moduleItem: ModuleItem): Boolean { - // If it's sequential progress and the group is unlocked, the first incomplete one can be viewed - // if this moduleItem is locked, it should be greyed out unless it is the first one (position == 1 -> it is 1 based, not - // 0 based) or the previous item is unlocked - if ((courseContext as Course).isTeacher || courseContext.isTA) { - return true - } - - if (moduleObject.sequentialProgress && moduleObject.state != null && (moduleObject.state == ModuleObject.State.Unlocked.apiString || moduleObject.state == ModuleObject.State.Started.apiString)) { - - //group is sequential, need to figure out which ones to grey out - val indexOfCurrentModuleItem = storedIndexOfItem(moduleObject, moduleItem) - if (indexOfCurrentModuleItem != -1) { - // getItem performs invalid index checks - val previousModuleItem = getItem(moduleObject, indexOfCurrentModuleItem - 1) - - return when { - isComplete(moduleItem) -> true - previousModuleItem == null -> true // Its the first one in the sequence - !isComplete(previousModuleItem) -> false // previous item is not complete - else -> isComplete(previousModuleItem) && !isComplete(moduleItem) // Previous complete, so show current as next in sequence - } - } - } - return true - } - - private fun isComplete(moduleItem: ModuleItem?): Boolean { - return moduleItem != null && moduleItem.completionRequirement != null && moduleItem.completionRequirement!!.completed - } - - // never actually shows prereqs because grayed out module items show instead. - private fun getPrerequisiteString(moduleObject: ModuleObject): String { - var prereqString = context.getString(R.string.noItemsToDisplayShort) - - if (ModuleUtility.isGroupLocked(moduleObject)) { - prereqString = context.getString(R.string.locked) - } - - if (moduleObject.state != null && - moduleObject.state == ModuleObject.State.Locked.apiString && - getGroupItemCount(moduleObject) > 0 && - getItem(moduleObject, 0)?.type == ModuleObject.State.UnlockRequirements.apiString) { - - val reqs = StringBuilder() - val ids = moduleObject.prerequisiteIds - //check to see if they need to finish other modules first - if (ids != null) { - for (i in ids.indices) { - val prereqModuleObject = getGroup(ids[i]) - if (prereqModuleObject != null) { - if (i == 0) { //if it's the first one, add the "Prerequisite:" label - reqs.append(context.getString(R.string.prerequisites) + " " + prereqModuleObject.name) - } else { - reqs.append(", " + prereqModuleObject.name!!) - } - } - } - } - - if (moduleObject.unlockAt != null) { - //only want a newline if there are prerequisite ids - if (ids!!.size > 0 && ids[0] != 0L) { - reqs.append("\n") - } - reqs.append(DateHelper.createPrefixedDateTimeString(context, R.string.unlocked, moduleObject.unlockDate)) - } - - prereqString = reqs.toString() - } - return prereqString - } - // endregion - - private abstract class ModuleItemCallback internal constructor(internal val moduleObject: ModuleObject) : StatusCallback>() { - internal var isFromNetwork = false // When true, there is no need to fetch objects from the network again. - } -} diff --git a/apps/student/src/main/java/com/instructure/student/di/ConferenceDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/ConferenceDetailsModule.kt new file mode 100644 index 0000000000..aa77fd00e1 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/ConferenceDetailsModule.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.di + +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.pandautils.room.offline.facade.ConferenceFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsRepository +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsLocalDataSource +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ConferenceDetailsModule { + + @Provides + fun provideNetworkDataSource( + conferencesApi: ConferencesApi.ConferencesInterface, + oAuthApi: OAuthAPI.OAuthInterface + ): ConferenceDetailsNetworkDataSource { + return ConferenceDetailsNetworkDataSource(conferencesApi, oAuthApi) + } + + @Provides + fun provideLocalDataSource( + conferenceFacade: ConferenceFacade + ): ConferenceDetailsLocalDataSource { + return ConferenceDetailsLocalDataSource(conferenceFacade) + } + + @Provides + fun provideRepository( + localDataSource: ConferenceDetailsLocalDataSource, + networkDataSource: ConferenceDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): ConferenceDetailsRepository { + return ConferenceDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/ConferenceListModule.kt b/apps/student/src/main/java/com/instructure/student/di/ConferenceListModule.kt new file mode 100644 index 0000000000..7ce82925a2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/ConferenceListModule.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.di + +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.pandautils.room.offline.facade.ConferenceFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListLocalDataSource +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ConferenceListModule { + + @Provides + fun provideNetworkDataSource( + conferencesApi: ConferencesApi.ConferencesInterface, + oAuthApi: OAuthAPI.OAuthInterface + ): ConferenceListNetworkDataSource { + return ConferenceListNetworkDataSource(conferencesApi, oAuthApi) + } + + @Provides + fun provideLocalDataSource( + conferenceFacade: ConferenceFacade + ): ConferenceListLocalDataSource { + return ConferenceListLocalDataSource(conferenceFacade) + } + + @Provides + fun provideRepository( + localDataSource: ConferenceListLocalDataSource, + networkDataSource: ConferenceListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): ConferenceListRepository { + return ConferenceListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt b/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt index 3526d7a9fa..e31afb88a7 100644 --- a/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/DashboardModule.kt @@ -17,8 +17,20 @@ package com.instructure.student.di import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.DashboardCardDao +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.dashboard.DashboardLocalDataSource +import com.instructure.student.features.dashboard.DashboardNetworkDataSource +import com.instructure.student.features.dashboard.DashboardRepository import com.instructure.student.features.dashboard.edit.StudentEditDashboardRouter import com.instructure.student.features.dashboard.notifications.StudentDashboardRouter import dagger.Module @@ -39,4 +51,30 @@ class DashboardModule { fun provideEditDashboardRouter(activity: FragmentActivity): EditDashboardRouter { return StudentEditDashboardRouter(activity) } + + @Provides + fun provideDashboardNetworkDataSource( + courseApi: CourseAPI.CoursesInterface, + groupApi: GroupAPI.GroupInterface, + apiPrefs: ApiPrefs + ): DashboardNetworkDataSource { + return DashboardNetworkDataSource(courseApi, groupApi, apiPrefs) + } + + @Provides + fun provideDashboardLocalDataSource(courseFacade: CourseFacade, dashboardCardDao: DashboardCardDao): DashboardLocalDataSource { + return DashboardLocalDataSource(courseFacade, dashboardCardDao) + } + + @Provides + fun provideDashboardRepository( + networkDataSource: DashboardNetworkDataSource, + localDataSource: DashboardLocalDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + courseSyncSettingsDao: CourseSyncSettingsDao, + courseDao: CourseDao + ): DashboardRepository { + return DashboardRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, courseSyncSettingsDao, courseDao) + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/DashboardViewModelModule.kt b/apps/student/src/main/java/com/instructure/student/di/DashboardViewModelModule.kt index f614863c03..29651eea8c 100644 --- a/apps/student/src/main/java/com/instructure/student/di/DashboardViewModelModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/DashboardViewModelModule.kt @@ -17,10 +17,18 @@ package com.instructure.student.di -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.GroupManager +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.EditDashboardItemDao +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.student.features.dashboard.edit.StudentEditDashboardRepository +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardLocalDataSource +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardNetworkDataSource import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -29,8 +37,27 @@ import dagger.hilt.android.components.ViewModelComponent @Module @InstallIn(ViewModelComponent::class) class DashboardViewModelModule { + + @Provides + fun provideStudentEditDashboardLocalDataSource(courseFacade: CourseFacade, + editDashboardItemDao: EditDashboardItemDao): StudentEditDashboardLocalDataSource { + return StudentEditDashboardLocalDataSource(courseFacade, editDashboardItemDao) + } + + @Provides + fun provideStudentEditDashboardNetworkDataSource(courseApi: CourseAPI.CoursesInterface, + groupApi: GroupAPI.GroupInterface): StudentEditDashboardNetworkDataSource { + return StudentEditDashboardNetworkDataSource(courseApi, groupApi) + } + @Provides - fun provideEditDashboardRepository(courseManager: CourseManager, groupManager: GroupManager): EditDashboardRepository { - return StudentEditDashboardRepository(courseManager, groupManager) + fun provideEditDashboardRepository(localDataSource: StudentEditDashboardLocalDataSource, + networkDataSource: StudentEditDashboardNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + courseSyncSettingsDao: CourseSyncSettingsDao, + courseDao: CourseDao + ): EditDashboardRepository { + return StudentEditDashboardRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, courseSyncSettingsDao, courseDao) } } diff --git a/apps/student/src/main/java/com/instructure/student/di/DiscussionHelperModule.kt b/apps/student/src/main/java/com/instructure/student/di/DiscussionHelperModule.kt new file mode 100644 index 0000000000..a43473bd0c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/DiscussionHelperModule.kt @@ -0,0 +1,27 @@ +package com.instructure.student.di + +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperLocalDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.routing.DiscussionRouteHelperStudentRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class DiscussionHelperModule { + + @Provides + fun provideDiscussionRouteHelperStudentRepository( + localDataSource: DiscussionRouteHelperLocalDataSource, + networkDataSource: DiscussionRouteHelperNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): DiscussionRouteHelperRepository { + return DiscussionRouteHelperStudentRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/DiscussionModule.kt b/apps/student/src/main/java/com/instructure/student/di/DiscussionModule.kt index bc22bb2dbf..d93219c4f0 100644 --- a/apps/student/src/main/java/com/instructure/student/di/DiscussionModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/DiscussionModule.kt @@ -2,7 +2,8 @@ package com.instructure.student.di import androidx.fragment.app.FragmentActivity import com.instructure.pandautils.features.discussion.router.DiscussionRouter -import com.instructure.student.features.discussion.StudentDiscussionRouter +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.routing.StudentDiscussionRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -13,7 +14,7 @@ import dagger.hilt.android.components.FragmentComponent class DiscussionModule { @Provides - fun provideDiscussionRouter(fragmentActivity: FragmentActivity): DiscussionRouter { - return StudentDiscussionRouter(fragmentActivity) + fun provideDiscussionRouter(fragmentActivity: FragmentActivity, networkStateProvider: NetworkStateProvider): DiscussionRouter { + return StudentDiscussionRouter(fragmentActivity, networkStateProvider) } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt b/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt index 32f4e8fe03..24914b2cd9 100644 --- a/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/LoginModule.kt @@ -19,6 +19,7 @@ package com.instructure.student.di import androidx.fragment.app.FragmentActivity import com.instructure.loginapi.login.LoginNavigation import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter +import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.student.features.login.StudentAcceptableUsePolicyRouter import com.instructure.student.features.login.StudentLoginNavigation import dagger.Module @@ -31,12 +32,12 @@ import dagger.hilt.android.components.ActivityComponent class LoginModule { @Provides - fun provideAcceptabelUsePolicyRouter(activity: FragmentActivity): AcceptableUsePolicyRouter { - return StudentAcceptableUsePolicyRouter(activity) + fun provideAcceptabelUsePolicyRouter(activity: FragmentActivity, databaseProvider: DatabaseProvider): AcceptableUsePolicyRouter { + return StudentAcceptableUsePolicyRouter(activity, databaseProvider) } @Provides - fun provideLoginNavigation(activity: FragmentActivity): LoginNavigation { - return StudentLoginNavigation(activity) + fun provideLoginNavigation(activity: FragmentActivity, databaseProvider: DatabaseProvider): LoginNavigation { + return StudentLoginNavigation(activity, databaseProvider) } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt b/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt index 7a215d7a6b..55d71e28c0 100644 --- a/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/NavigationActivityModule.kt @@ -17,7 +17,23 @@ package com.instructure.student.di import androidx.fragment.app.FragmentActivity +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.apis.UserAPI import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.grades.datasource.GradesListLocalDataSource +import com.instructure.student.features.grades.datasource.GradesListNetworkDataSource +import com.instructure.student.features.navigation.NavigationRepository +import com.instructure.student.features.navigation.datasource.NavigationLocalDataSource +import com.instructure.student.features.navigation.datasource.NavigationNetworkDataSource import com.instructure.student.navigation.DefaultNavigationBehavior import com.instructure.student.navigation.ElementaryNavigationBehavior import com.instructure.student.navigation.NavigationBehavior @@ -55,4 +71,29 @@ class NavigationActivityModule { fun provideAppShortcutManager(): AppShortcutManager { return DefaultAppShortcutManager() } + + @Provides + fun provideNavigationRepository( + localDataSource: NavigationLocalDataSource, + networkDataSource: NavigationNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): NavigationRepository { + return NavigationRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } + + @Provides + fun provideNavigationLocalDataSource( + courseFacade: CourseFacade + ): NavigationLocalDataSource { + return NavigationLocalDataSource(courseFacade) + } + + @Provides + fun provideNavigationNetworkDataSource( + courseApi: CourseAPI.CoursesInterface, + userApi: UserAPI.UsersInterface + ): NavigationNetworkDataSource { + return NavigationNetworkDataSource(courseApi, userApi) + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt b/apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt new file mode 100644 index 0000000000..f6a1fa2200 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/OfflineModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.di + +import com.instructure.pandautils.features.offline.sync.SyncRouter +import com.instructure.student.features.offline.sync.StudentSyncRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class OfflineModule { + + @Provides + fun provideSyncRouter(): SyncRouter { + return StudentSyncRouter() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/SubmissionDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/SubmissionDetailsModule.kt new file mode 100644 index 0000000000..1ccd23e441 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/SubmissionDetailsModule.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.di + +import com.instructure.canvasapi2.apis.* +import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsRepository +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsLocalDataSource +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class SubmissionDetailsModule { + + @Provides + fun provideNetworkDataSource( + enrollmentApi: EnrollmentAPI.EnrollmentInterface, + submissionApi: SubmissionAPI.SubmissionInterface, + assignmentApi: AssignmentAPI.AssignmentInterface, + quizApi: QuizAPI.QuizInterface, + featuresApi: FeaturesAPI.FeaturesInterface, + courseApi: CourseAPI.CoursesInterface + ): SubmissionDetailsNetworkDataSource { + return SubmissionDetailsNetworkDataSource(enrollmentApi, submissionApi, assignmentApi, quizApi, featuresApi, courseApi) + } + + @Provides + fun provideLocalDataSource( + enrollmentFacade: EnrollmentFacade, + submissionFacade: SubmissionFacade, + assignmentFacade: AssignmentFacade, + quizDao: QuizDao, + courseFeaturesDao: CourseFeaturesDao, + courseSettingsDao: CourseSettingsDao + ): SubmissionDetailsLocalDataSource { + return SubmissionDetailsLocalDataSource(enrollmentFacade, submissionFacade, assignmentFacade, quizDao, courseFeaturesDao, courseSettingsDao) + } + + @Provides + fun provideRepository( + localDataSource: SubmissionDetailsLocalDataSource, + networkDataSource: SubmissionDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): SubmissionDetailsRepository { + return SubmissionDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt b/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt new file mode 100644 index 0000000000..d96591ea7b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/SyllabusModule.kt @@ -0,0 +1,48 @@ +package com.instructure.student.di + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.syllabus.SyllabusRepository +import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource +import com.instructure.student.mobius.syllabus.datasource.SyllabusNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class SyllabusModule { + + @Provides + fun provideNetworkDataSource( + courseApi: CourseAPI.CoursesInterface, + calendarEventApi: CalendarEventAPI.CalendarEventInterface + ): SyllabusNetworkDataSource { + return SyllabusNetworkDataSource(courseApi, calendarEventApi) + } + + @Provides + fun provideLocalDataSource( + courseSettingsDao: CourseSettingsDao, + courseFacade: CourseFacade, + scheduleItemFacade: ScheduleItemFacade + ): SyllabusLocalDataSource { + return SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade) + } + + @Provides + fun provideSyllabusRepository( + localDataSource: SyllabusLocalDataSource, + networkDataSource: SyllabusNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): SyllabusRepository { + return SyllabusRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt new file mode 100644 index 0000000000..05e0815954 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentDetailsModule.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.* +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.assignments.details.AssignmentDetailsRepository +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class AssignmentDetailsModule { + + @Provides + fun provideAssignmentDetailsLocalDataSource( + courseFacade: CourseFacade, + assignmentFacade: AssignmentFacade, + quizDao: QuizDao + ): AssignmentDetailsLocalDataSource { + return AssignmentDetailsLocalDataSource(courseFacade, assignmentFacade, quizDao) + } + + @Provides + fun provideAssignmentDetailsNetworkDataSource( + coursesInterface: CourseAPI.CoursesInterface, + assignmentInterface: AssignmentAPI.AssignmentInterface, + quizInterface: QuizAPI.QuizInterface, + submissionInterface: SubmissionAPI.SubmissionInterface + ): AssignmentDetailsNetworkDataSource { + return AssignmentDetailsNetworkDataSource(coursesInterface, assignmentInterface, quizInterface, submissionInterface) + } + + @Provides + fun provideCourseBrowserRepository( + networkStateProvider: NetworkStateProvider, + localDataSource: AssignmentDetailsLocalDataSource, + networkDataSource: AssignmentDetailsNetworkDataSource, + featureFlagProvider: FeatureFlagProvider + ): AssignmentDetailsRepository { + return AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentListModule.kt new file mode 100644 index 0000000000..42acf1f5c6 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/AssignmentListModule.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.assignments.list.AssignmentListRepository +import com.instructure.student.features.assignments.list.datasource.AssignmentListLocalDataSource +import com.instructure.student.features.assignments.list.datasource.AssignmentListNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class AssignmentListModule { + + @Provides + fun provideAssignmentListLocalDataSource( + assignmentFacade: AssignmentFacade, + courseFacade: CourseFacade, + courseSettingsDao: CourseSettingsDao + ): AssignmentListLocalDataSource { + return AssignmentListLocalDataSource(assignmentFacade, courseFacade, courseSettingsDao) + } + + @Provides + fun provideAssignmentListNetworkDataSource( + assignmentApi: AssignmentAPI.AssignmentInterface, + courseApi: CourseAPI.CoursesInterface + ): AssignmentListNetworkDataSource { + return AssignmentListNetworkDataSource(assignmentApi, courseApi) + } + + @Provides + fun provideAssignmentListRepository( + localDataSource: AssignmentListLocalDataSource, + networkDataSource: AssignmentListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): AssignmentListRepository { + return AssignmentListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/CourseBrowserModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/CourseBrowserModule.kt new file mode 100644 index 0000000000..ea0d814e7b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/CourseBrowserModule.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.coursebrowser.CourseBrowserRepository +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserLocalDataSource +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class CourseBrowserModule { + + @Provides + fun provideCourseBrowserLocalDataSource( + tabDao: TabDao, + pageFacade: PageFacade, + courseSyncSettingsDao: CourseSyncSettingsDao, + fileSyncSettingsDao: FileSyncSettingsDao + ): CourseBrowserLocalDataSource { + return CourseBrowserLocalDataSource( + tabDao, + pageFacade, + courseSyncSettingsDao, + fileSyncSettingsDao + ) + } + + @Provides + fun provideCourseBrowserNetworkDataSource( + tabApi: TabAPI.TabsInterface, + pageApi: PageAPI.PagesInterface + ): CourseBrowserNetworkDataSource { + return CourseBrowserNetworkDataSource(tabApi, pageApi) + } + + @Provides + fun provideCourseBrowserRepository( + networkDataSource: CourseBrowserNetworkDataSource, + localDataSource: CourseBrowserLocalDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): CourseBrowserRepository { + return CourseBrowserRepository(networkDataSource, localDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/DiscussionDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/DiscussionDetailsModule.kt new file mode 100644 index 0000000000..e5deb7031c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/DiscussionDetailsModule.kt @@ -0,0 +1,53 @@ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.DiscussionTopicFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.details.DiscussionDetailsRepository +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsLocalDataSource +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class DiscussionDetailsModule { + @Provides + fun provideDiscussionDetailsLocalDataSource( + discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + discussionTopicFacade: DiscussionTopicFacade, + courseSettingsDao: CourseSettingsDao, + groupFacade: GroupFacade, + ): DiscussionDetailsLocalDataSource { + return DiscussionDetailsLocalDataSource(discussionTopicHeaderFacade, discussionTopicFacade, courseSettingsDao, groupFacade) + } + + @Provides + fun provideDiscussionDetailsNetworkDataSource( + discussionApi: DiscussionAPI.DiscussionInterface, + oAuthApi: OAuthAPI.OAuthInterface, + courseApi: CourseAPI.CoursesInterface, + groupApi: GroupAPI.GroupInterface, + ): DiscussionDetailsNetworkDataSource { + return DiscussionDetailsNetworkDataSource(discussionApi, oAuthApi, courseApi, groupApi) + } + + @Provides + fun provideDiscussionDetailsRepository( + localDataSource: DiscussionDetailsLocalDataSource, + networkDataSource: DiscussionDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): DiscussionDetailsRepository { + return DiscussionDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/DiscussionListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/DiscussionListModule.kt new file mode 100644 index 0000000000..98a16f93b2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/DiscussionListModule.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.list.DiscussionListRepository +import com.instructure.student.features.discussion.list.datasource.DiscussionListLocalDataSource +import com.instructure.student.features.discussion.list.datasource.DiscussionListNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class DiscussionListModule { + + @Provides + fun provideDiscussionListLocalDataSource(discussionTopicHeaderFacade: DiscussionTopicHeaderFacade): DiscussionListLocalDataSource { + return DiscussionListLocalDataSource(discussionTopicHeaderFacade) + } + + @Provides + fun provideDiscussionListNetworkDataSource( + courseApi: CourseAPI.CoursesInterface, + groupApi: GroupAPI.GroupInterface, + discussionApi: DiscussionAPI.DiscussionInterface, + announcementApi: AnnouncementAPI.AnnouncementInterface + ): DiscussionListNetworkDataSource { + return DiscussionListNetworkDataSource(courseApi, groupApi, discussionApi, announcementApi) + } + + @Provides + fun provideDiscussionListRepository( + localDataSource: DiscussionListLocalDataSource, + networkDataSource: DiscussionListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): DiscussionListRepository { + return DiscussionListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/FileDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/FileDetailsModule.kt new file mode 100644 index 0000000000..c929ebb930 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/FileDetailsModule.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.files.details.FileDetailsLocalDataSource +import com.instructure.student.features.files.details.FileDetailsNetworkDataSource +import com.instructure.student.features.files.details.FileDetailsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class FileDetailsModule { + + @Provides + fun provideFileDetailsNetworkDataSource(moduleApi: ModuleAPI.ModuleInterface, fileFolderApi: FileFolderAPI.FilesFoldersInterface): FileDetailsNetworkDataSource { + return FileDetailsNetworkDataSource(moduleApi, fileFolderApi) + } + + @Provides + fun provideFileDetailsLocalDataSource(fileFolderDao: FileFolderDao, localFileDao: LocalFileDao): FileDetailsLocalDataSource { + return FileDetailsLocalDataSource(fileFolderDao, localFileDao) + } + + @Provides + fun provideFileDetailsRepository( + localDataSource:FileDetailsLocalDataSource, + networkDataSource: FileDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + ): FileDetailsRepository { + return FileDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } + +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/FileListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/FileListModule.kt new file mode 100644 index 0000000000..6bac559fec --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/FileListModule.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.files.list.FileListLocalDataSource +import com.instructure.student.features.files.list.FileListNetworkDataSource +import com.instructure.student.features.files.list.FileListRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class FileListModule { + + @Provides + fun provideNetworkDataSource(fileFolderApi: FileFolderAPI.FilesFoldersInterface): FileListNetworkDataSource { + return FileListNetworkDataSource(fileFolderApi) + } + + @Provides + fun provideLocalDataSource(fileFolderDao: FileFolderDao, localFileDao: LocalFileDao): FileListLocalDataSource { + return FileListLocalDataSource(fileFolderDao, localFileDao) + } + + @Provides + fun provideFileListRepository( + fileListLocalDataSource: FileListLocalDataSource, + fileListNetworkDataSource: FileListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): FileListRepository { + return FileListRepository( + fileListLocalDataSource, + fileListNetworkDataSource, + networkStateProvider, + featureFlagProvider + ) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/GradesListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/GradesListModule.kt new file mode 100644 index 0000000000..1888a69a4b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/GradesListModule.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.grades.GradesListRepository +import com.instructure.student.features.grades.datasource.GradesListLocalDataSource +import com.instructure.student.features.grades.datasource.GradesListNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class GradesListModule { + + @Provides + fun provideGradesListLocalDataSource( + courseFacade: CourseFacade, + enrollmentFacade: EnrollmentFacade, + assignmentFacade: AssignmentFacade, + submissionFacade: SubmissionFacade + ): GradesListLocalDataSource { + return GradesListLocalDataSource(courseFacade, enrollmentFacade, assignmentFacade, submissionFacade) + } + + @Provides + fun provideGradesListNetworkDataSource( + courseApi: CourseAPI.CoursesInterface, + enrollmentApi: EnrollmentAPI.EnrollmentInterface, + assignmentApi: AssignmentAPI.AssignmentInterface, + submissionApi: SubmissionAPI.SubmissionInterface + ): GradesListNetworkDataSource { + return GradesListNetworkDataSource(courseApi, enrollmentApi, assignmentApi, submissionApi) + } + + @Provides + fun provideGradesListRepository( + localDataSource: GradesListLocalDataSource, + networkDataSource: GradesListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): GradesListRepository { + return GradesListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt new file mode 100644 index 0000000000..4420911574 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/ModuleListModule.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.modules.list.ModuleListRepository +import com.instructure.student.features.modules.list.datasource.ModuleListLocalDataSource +import com.instructure.student.features.modules.list.datasource.ModuleListNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ModuleListModule { + + @Provides + fun provideModuleListLocalDataSource(tabDao: TabDao, moduleFacade: ModuleFacade, courseSettingsDao: CourseSettingsDao): ModuleListLocalDataSource { + return ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao) + } + + @Provides + fun provideModuleListNetworkDataSource(moduleApi: ModuleAPI.ModuleInterface, tabApi: TabAPI.TabsInterface, courseApi: CourseAPI.CoursesInterface): ModuleListNetworkDataSource { + return ModuleListNetworkDataSource(moduleApi, tabApi, courseApi) + } + + @Provides + fun provideModuleListRepository( + moduleListLocalDataSource: ModuleListLocalDataSource, + moduleListNetworkDataSource: ModuleListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): ModuleListRepository { + return ModuleListRepository(moduleListLocalDataSource, moduleListNetworkDataSource, networkStateProvider, featureFlagProvider) + } + +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/ModuleProgressionModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/ModuleProgressionModule.kt new file mode 100644 index 0000000000..ad6a3fb319 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/ModuleProgressionModule.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.modules.progression.ModuleProgressionRepository +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionLocalDataSource +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class ModuleProgressionModule { + + @Provides + fun provideModuleProgressionLocalDataSource(moduleFacade: ModuleFacade, quizDao: QuizDao): ModuleProgressionLocalDataSource { + return ModuleProgressionLocalDataSource(moduleFacade, quizDao) + } + + @Provides + fun provideModuleProgressionNetworkDataSource(moduleApi: ModuleAPI.ModuleInterface, quizApi: QuizAPI.QuizInterface): ModuleProgressionNetworkDataSource { + return ModuleProgressionNetworkDataSource(moduleApi, quizApi) + } + + @Provides + fun provideModuleProgressionRepository( + moduleProgressionLocalDataSource: ModuleProgressionLocalDataSource, + moduleProgressionNetworkDataSource: ModuleProgressionNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + courseSyncSettingsDao: CourseSyncSettingsDao, + localFileDao: LocalFileDao + ): ModuleProgressionRepository { + return ModuleProgressionRepository( + moduleProgressionLocalDataSource, + moduleProgressionNetworkDataSource, + networkStateProvider, + featureFlagProvider, + courseSyncSettingsDao, + localFileDao + ) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/PageDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/PageDetailsModule.kt new file mode 100644 index 0000000000..e29eac1f17 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/PageDetailsModule.kt @@ -0,0 +1,38 @@ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.pages.details.PageDetailsRepository +import com.instructure.student.features.pages.details.datasource.PageDetailsLocalDataSource +import com.instructure.student.features.pages.details.datasource.PageDetailsNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class PageDetailsModule { + + @Provides + fun provideLocalDataSource(pageFacade: PageFacade): PageDetailsLocalDataSource { + return PageDetailsLocalDataSource(pageFacade) + } + + @Provides + fun provideNetworkDataSource(pageApi: PageAPI.PagesInterface): PageDetailsNetworkDataSource { + return PageDetailsNetworkDataSource(pageApi) + } + + @Provides + fun providePageDetailsRepository( + localDataSource: PageDetailsLocalDataSource, + networkDataSource: PageDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): PageDetailsRepository { + return PageDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/PageListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/PageListModule.kt new file mode 100644 index 0000000000..588d3d9e9c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/PageListModule.kt @@ -0,0 +1,38 @@ +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.pages.list.PageListRepository +import com.instructure.student.features.pages.list.datasource.PageListLocalDataSource +import com.instructure.student.features.pages.list.datasource.PageListNetworkDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class PageListModule { + + @Provides + fun provideLocalDataSource(pageFacade: PageFacade): PageListLocalDataSource { + return PageListLocalDataSource(pageFacade) + } + + @Provides + fun provideNetworkDataSource(pageApi: PageAPI.PagesInterface): PageListNetworkDataSource { + return PageListNetworkDataSource(pageApi) + } + + @Provides + fun providePageListRepository( + localDataSource: PageListLocalDataSource, + networkDataSource: PageListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): PageListRepository { + return PageListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/PeopleDetailsModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/PeopleDetailsModule.kt new file mode 100644 index 0000000000..8a7f7cc53a --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/PeopleDetailsModule.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.pandautils.room.offline.facade.UserFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.people.details.PeopleDetailsLocalDataSource +import com.instructure.student.features.people.details.PeopleDetailsNetworkDataSource +import com.instructure.student.features.people.details.PeopleDetailsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class PeopleDetailsModule { + @Provides + fun provideLocalDataSource(userFacade: UserFacade): PeopleDetailsLocalDataSource { + return PeopleDetailsLocalDataSource(userFacade) + } + + @Provides + fun provideNetworkDataSource(userApi: UserAPI.UsersInterface, courseApi: CourseAPI.CoursesInterface, groupApi: GroupAPI.GroupInterface): PeopleDetailsNetworkDataSource { + return PeopleDetailsNetworkDataSource(userApi, courseApi, groupApi) + } + + @Provides + fun providePeopleDetailsRepository( + localDataSource: PeopleDetailsLocalDataSource, + networkDataSource: PeopleDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): PeopleDetailsRepository { + return PeopleDetailsRepository(networkDataSource, localDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/PeopleListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/PeopleListModule.kt new file mode 100644 index 0000000000..475c7c6b4e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/PeopleListModule.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.pandautils.room.offline.facade.UserFacade +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.people.list.PeopleListLocalDataSource +import com.instructure.student.features.people.list.PeopleListNetworkDataSource +import com.instructure.student.features.people.list.PeopleListRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class PeopleListModule { + @Provides + fun provideLocalDataSource(userFacade: UserFacade): PeopleListLocalDataSource { + return PeopleListLocalDataSource(userFacade) + } + + @Provides + fun provideNetworkDataSource(userAPI: UserAPI.UsersInterface): PeopleListNetworkDataSource { + return PeopleListNetworkDataSource(userAPI) + } + + @Provides + fun providePeopleListRepository( + localDataSource: PeopleListLocalDataSource, + networkDataSource: PeopleListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): PeopleListRepository { + return PeopleListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/di/feature/QuizListModule.kt b/apps/student/src/main/java/com/instructure/student/di/feature/QuizListModule.kt new file mode 100644 index 0000000000..71010a0807 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/di/feature/QuizListModule.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.di.feature + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.quiz.list.QuizListLocalDataSource +import com.instructure.student.features.quiz.list.QuizListNetworkDataSource +import com.instructure.student.features.quiz.list.QuizListRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.FragmentComponent + +@Module +@InstallIn(FragmentComponent::class) +class QuizListModule() { + + @Provides + fun provideQuizListNetworkDataSource(quizApi: QuizAPI.QuizInterface, courseApi: CourseAPI.CoursesInterface): QuizListNetworkDataSource { + return QuizListNetworkDataSource(quizApi, courseApi) + } + + @Provides + fun provideQuizListLocalDataSource(quizDao: QuizDao, courseSettingsDao: CourseSettingsDao): QuizListLocalDataSource { + return QuizListLocalDataSource(quizDao, courseSettingsDao) + } + + @Provides + fun provideQuizListRepository( + localDataSource: QuizListLocalDataSource, + networkDataSource: QuizListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider + ): QuizListRepository { + return QuizListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt similarity index 95% rename from apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt index 5b3bc033fe..5dc83c242f 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.features.assignmentdetails +package com.instructure.student.features.assignments.details import android.app.Dialog import android.net.Uri @@ -33,7 +33,6 @@ import com.instructure.canvasapi2.models.Assignment.SubmissionType import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam -import com.instructure.interactions.Navigation import com.instructure.interactions.bookmarks.Bookmarkable import com.instructure.interactions.bookmarks.Bookmarker import com.instructure.interactions.router.Route @@ -60,7 +59,7 @@ import com.instructure.student.mobius.assignmentDetails.submission.picker.Picker import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment -import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.getResourceSelectorUrl import dagger.hilt.android.AndroidEntryPoint @@ -81,7 +80,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { private val captureVideoContract = registerForActivityResult(ActivityResultContracts.CaptureVideo()) { val assignment = viewModel.assignment if (assignment != null && captureVideoUri != null && it) { - RouteMatcher.route(requireContext(), PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, captureVideoUri!!)) + RouteMatcher.route(requireActivity(), PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, captureVideoUri!!)) } else { toast(R.string.videoRecordingError) } @@ -90,7 +89,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { private val mediaPickerContract = registerForActivityResult(ActivityResultContracts.OpenDocument()) { val assignment = viewModel.assignment if (assignment != null && it != null) { - RouteMatcher.route(requireContext(), PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, it)) + RouteMatcher.route(requireActivity(), PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, it)) } else { toast(R.string.unexpectedErrorOpeningFile) } @@ -107,12 +106,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { title = context?.getString(R.string.assignmentDetails) subtitle = viewModel.course?.name - val navigation = activity as? Navigation - if (navigation?.canBookmark().orDefault(true)) { - setMenu(R.menu.bookmark_menu) { - navigation?.addBookmark() - } - } + setupToolbarMenu(this) ViewStyler.themeToolbarColored(requireActivity(), this, viewModel.course) } @@ -156,7 +150,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { is AssignmentDetailAction.NavigateToSubmissionScreen -> { RouteMatcher.route( requireActivity(), - SubmissionDetailsFragment.makeRoute(canvasContext, assignmentId, action.isObserver, action.selectedSubmissionAttempt) + SubmissionDetailsRepositoryFragment.makeRoute(canvasContext, assignmentId, action.isObserver, action.selectedSubmissionAttempt) ) } is AssignmentDetailAction.NavigateToQuizScreen -> { @@ -369,7 +363,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { binding?.descriptionWebViewWrapper?.webView?.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { override fun launchInternalWebViewFragment(url: String) { InternalWebviewFragment.loadInternalWebView( - context, + requireActivity(), InternalWebviewFragment.makeRoute(canvasContext, url, false) ) } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt new file mode 100644 index 0000000000..0a7d26035d --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsRepository.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.details + +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsDataSource +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource + +class AssignmentDetailsRepository( + localDataSource: AssignmentDetailsLocalDataSource, + networkDataSource: AssignmentDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course { + return dataSource().getCourseWithGrade(courseId, forceNetwork) + } + + suspend fun getAssignment(isObserver: Boolean, assignmentId: Long, courseId: Long, forceNetwork: Boolean): Assignment { + return dataSource().getAssignment(isObserver, assignmentId, courseId, forceNetwork) + } + + suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz { + return dataSource().getQuiz(courseId, quizId, forceNetwork) + } + + suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): LTITool? { + return dataSource().getExternalToolLaunchUrl(courseId, externalToolId, assignmentId, forceNetwork) + } + + suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? { + return dataSource().getLtiFromAuthenticationUrl(url, forceNetwork) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewData.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt similarity index 95% rename from apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewData.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt index b52e1982a3..5058b054a4 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewData.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewData.kt @@ -1,4 +1,4 @@ -package com.instructure.student.features.assignmentdetails +package com.instructure.student.features.assignments.details import android.text.Spanned import androidx.annotation.ColorRes @@ -7,7 +7,7 @@ import androidx.databinding.Bindable import com.instructure.canvasapi2.models.* import com.instructure.pandautils.features.assignmentdetails.AssignmentDetailsAttemptItemViewModel import com.instructure.pandautils.utils.ThemedColor -import com.instructure.student.features.assignmentdetails.gradecellview.GradeCellViewData +import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData data class AssignmentDetailsViewData( val courseColor: ThemedColor, diff --git a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt similarity index 90% rename from apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt index f9c07cd306..345659d12b 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt @@ -15,15 +15,12 @@ * */ -package com.instructure.student.features.assignmentdetails +package com.instructure.student.features.assignments.details import android.app.Application import android.content.Context import android.content.res.Resources import androidx.lifecycle.* -import com.instructure.canvasapi2.managers.AssignmentManager -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.QuizManager import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.Assignment.SubmissionType @@ -38,7 +35,7 @@ import com.instructure.pandautils.mvvm.ViewState import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.db.StudentDb -import com.instructure.student.features.assignmentdetails.gradecellview.GradeCellViewData +import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData import com.instructure.student.mobius.assignmentDetails.getFormattedAttemptDate import com.instructure.student.mobius.assignmentDetails.uploadAudioRecording import com.instructure.student.util.getStudioLTITool @@ -54,10 +51,7 @@ import com.instructure.student.Submission as DatabaseSubmission @HiltViewModel class AssignmentDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val courseManager: CourseManager, - private val assignmentManager: AssignmentManager, - private val quizManager: QuizManager, - private val submissionManager: SubmissionManager, + private val assignmentDetailsRepository: AssignmentDetailsRepository, private val resources: Resources, private val htmlContentFormatter: HtmlContentFormatter, private val colorKeeper: ColorKeeper, @@ -104,6 +98,7 @@ class AssignmentDetailsViewModel @Inject constructor( init { markSubmissionAsRead() submissionQuery.addListener(this) + _state.postValue(ViewState.Loading) loadData() } @@ -121,13 +116,15 @@ class AssignmentDetailsViewModel @Inject constructor( if (!isDraft && !isUploading) { isUploading = true _data.value?.attempts = attempts?.toMutableList()?.apply { - add(0, AssignmentDetailsAttemptItemViewModel( - AssignmentDetailsAttemptViewData( - resources.getString(R.string.attempt, attempts.size + 1), - dateString, - isUploading = true + add( + 0, AssignmentDetailsAttemptItemViewModel( + AssignmentDetailsAttemptViewData( + resources.getString(R.string.attempt, attempts.size + 1), + dateString, + isUploading = true + ) ) - )) + ) }.orEmpty() _data.value?.notifyPropertyChanged(BR.attempts) } @@ -161,39 +158,31 @@ class AssignmentDetailsViewModel @Inject constructor( } private fun loadData(forceNetwork: Boolean = false) { - _state.postValue(ViewState.Loading) viewModelScope.launch { try { - val courseResult = courseManager.getCourseWithGradeAsync(course?.id.orDefault(), forceNetwork).await().dataOrThrow + val courseResult = assignmentDetailsRepository.getCourseWithGrade(course?.id.orDefault(), forceNetwork) restrictQuantitativeData = courseResult.settings?.restrictQuantitativeData ?: false gradingScheme = courseResult.gradingScheme isObserver = courseResult.enrollments?.firstOrNull { it.isObserver } != null - val assignmentResult = if (isObserver) { - assignmentManager.getAssignmentIncludeObserveesAsync( - assignmentId, - course?.id.orDefault(), - forceNetwork - ).await().dataOrThrow.toAssignmentForObservee() - } else { - assignmentManager.getAssignmentWithHistoryAsync( - assignmentId, - course?.id.orDefault(), - forceNetwork - ).await().dataOrThrow - } as Assignment + val assignmentResult = assignmentDetailsRepository.getAssignment( + isObserver, + assignmentId, + course?.id.orDefault(), + forceNetwork + ) quizResult = if (assignmentResult.turnInType == Assignment.TurnInType.QUIZ && assignmentResult.quizId != 0L) { - quizManager.getQuizAsync(course?.id.orDefault(), assignmentResult.quizId, forceNetwork).await().dataOrThrow + assignmentDetailsRepository.getQuiz(course?.id.orDefault(), assignmentResult.quizId, forceNetwork) } else null val ltiToolId = assignmentResult.externalToolAttributes?.contentId.orDefault() externalLTITool = if (ltiToolId != 0L) { - assignmentManager.getExternalToolLaunchUrlAsync(course?.id.orDefault(), ltiToolId, assignmentId).await().dataOrThrow + assignmentDetailsRepository.getExternalToolLaunchUrl(course?.id.orDefault(), ltiToolId, assignmentId, forceNetwork) } else { if (!assignmentResult.url.isNullOrEmpty() && assignmentResult.getSubmissionTypes().contains(SubmissionType.EXTERNAL_TOOL)) { - submissionManager.getLtiFromAuthenticationUrlAsync(assignmentResult.url.orEmpty(), forceNetwork).await().dataOrThrow + assignmentDetailsRepository.getLtiFromAuthenticationUrl(assignmentResult.url.orEmpty(), forceNetwork) } else { null } @@ -217,7 +206,12 @@ class AssignmentDetailsViewModel @Inject constructor( _data.postValue(getViewData(assignmentResult, hasDraft)) _state.postValue(ViewState.Success) } catch (ex: Exception) { - _state.postValue(ViewState.Error(resources.getString(R.string.errorLoadingAssignment))) + val errorString = if (ex is IllegalAccessException) { + resources.getString(R.string.assignmentNoLongerAvailable) + } else { + resources.getString(R.string.errorLoadingAssignment) + } + _state.postValue(ViewState.Error(errorString)) } } } @@ -225,12 +219,7 @@ class AssignmentDetailsViewModel @Inject constructor( private fun refreshAssignment() { viewModelScope.launch { try { - val assignmentResult = if (isObserver) { - assignmentManager.getAssignmentIncludeObserveesAsync(assignmentId, course?.id.orDefault(), true) - } else { - assignmentManager.getAssignmentWithHistoryAsync(assignmentId, course?.id.orDefault(), true) - }.await().dataOrThrow as Assignment - + val assignmentResult = assignmentDetailsRepository.getAssignment(isObserver, assignmentId, course?.id.orDefault(), true) _data.postValue(getViewData(assignmentResult, dbSubmission?.isDraft.orDefault())) } catch (e: Exception) { _events.value = Event(AssignmentDetailAction.ShowToast(resources.getString(R.string.assignmentRefreshError))) @@ -435,7 +424,7 @@ class AssignmentDetailsViewModel @Inject constructor( submissionStatusVisible = submissionStatusVisible, lockedMessage = partialLockedMessage, submitButtonText = submitButtonText, - submitEnabled = submitEnabled, + submitEnabled = (submitEnabled && assignmentDetailsRepository.isOnline()) || (submitEnabled && assignment.turnInType == Assignment.TurnInType.DISCUSSION), submitVisible = submitVisible, attempts = attempts, selectedGradeCellViewData = GradeCellViewData.fromSubmission( @@ -463,6 +452,7 @@ class AssignmentDetailsViewModel @Inject constructor( } fun refresh() { + _state.postValue(ViewState.Refresh) loadData(true) } @@ -562,4 +552,8 @@ class AssignmentDetailsViewModel @Inject constructor( postAction(AssignmentDetailAction.ShowToast(resources.getString(R.string.audioRecordingError))) } } + + fun showContent(viewState: ViewState?): Boolean { + return (viewState == ViewState.Success || viewState == ViewState.Refresh) && assignment != null + } } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsDataSource.kt new file mode 100644 index 0000000000..0ffdd1747b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsDataSource.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.details.datasource + +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.Quiz + +interface AssignmentDetailsDataSource { + suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course + suspend fun getAssignment(isObserver: Boolean, assignmentId: Long, courseId: Long, forceNetwork: Boolean): Assignment + suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz + suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): LTITool? + suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsLocalDataSource.kt new file mode 100644 index 0000000000..8bb16aff99 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsLocalDataSource.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.details.datasource + +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade + +class AssignmentDetailsLocalDataSource( + private val courseFacade: CourseFacade, + private val assignmentFacade: AssignmentFacade, + private val quizDao: QuizDao +) : AssignmentDetailsDataSource { + + override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course { + return courseFacade.getCourseById(courseId) ?: throw IllegalStateException("Could not load from DB") + } + + override suspend fun getAssignment(isObserver: Boolean, assignmentId: Long, courseId: Long, forceNetwork: Boolean): Assignment { + return assignmentFacade.getAssignmentById(assignmentId) ?: throw IllegalStateException("Could not load from DB") + } + + override suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz { + return quizDao.findById(quizId)?.toApiModel() ?: throw IllegalStateException("Could not load from DB") + } + + override suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): LTITool? { + return null + } + + override suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool? { + return null + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..f8e6ea0161 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/datasource/AssignmentDetailsNetworkDataSource.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.details.datasource + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.Failure + +class AssignmentDetailsNetworkDataSource( + private val coursesInterface: CourseAPI.CoursesInterface, + private val assignmentInterface: AssignmentAPI.AssignmentInterface, + private val quizInterface: QuizAPI.QuizInterface, + private val submissionInterface: SubmissionAPI.SubmissionInterface +) : AssignmentDetailsDataSource { + + override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return coursesInterface.getCourseWithGrade(courseId, params).dataOrThrow + } + + override suspend fun getAssignment(isObserver: Boolean, assignmentId: Long, courseId: Long, forceNetwork: Boolean): Assignment { + val assignmentResult = if (isObserver) { + getAssignmentIncludeObservees(assignmentId, courseId, forceNetwork) + } else { + getAssignmentWithHistory(assignmentId, courseId, forceNetwork) + } + + if (assignmentResult is DataResult.Fail && assignmentResult.failure is Failure.Authorization) { + throw IllegalAccessException("User is not authorized to view this assignment") + } else { + return assignmentResult.dataOrThrow + } + } + + override suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): Quiz { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return quizInterface.getQuiz(courseId, quizId, params).dataOrThrow + } + + override suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): LTITool { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return assignmentInterface.getExternalToolLaunchUrl(courseId, externalToolId, assignmentId, restParams = params).dataOrThrow + } + + override suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): LTITool { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return submissionInterface.getLtiFromAuthenticationUrl(url, params).dataOrThrow + } + + private suspend fun getAssignmentIncludeObservees(assignmentId: Long, courseId: Long, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return assignmentInterface.getAssignmentIncludeObservees(courseId, assignmentId, params).map { it.toAssignmentForObservee() } + } + + private suspend fun getAssignmentWithHistory(assignmentId: Long, courseId: Long, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return assignmentInterface.getAssignmentWithHistory(courseId, assignmentId, params) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/gradecellview/GradeCellViewData.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt similarity index 99% rename from apps/student/src/main/java/com/instructure/student/features/assignmentdetails/gradecellview/GradeCellViewData.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt index 38d67e8b48..7fb3b6eae8 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/gradecellview/GradeCellViewData.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/gradecellview/GradeCellViewData.kt @@ -1,4 +1,4 @@ -package com.instructure.student.features.assignmentdetails.gradecellview +package com.instructure.student.features.assignments.details.gradecellview import android.content.res.Resources import androidx.core.graphics.ColorUtils diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt similarity index 91% rename from apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt index 59d9c327e7..e198678fb9 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.assignments.list import android.content.DialogInterface import android.content.res.Configuration @@ -47,23 +47,30 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.TermSpinnerAdapter -import com.instructure.student.adapter.assignment.AssignmentListByDateRecyclerAdapter -import com.instructure.student.adapter.assignment.AssignmentListByTypeRecyclerAdapter -import com.instructure.student.adapter.assignment.AssignmentListFilter -import com.instructure.student.adapter.assignment.AssignmentListRecyclerAdapter import com.instructure.student.databinding.AssignmentListLayoutBinding -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.assignments.list.adapter.AssignmentListByDateRecyclerAdapter +import com.instructure.student.features.assignments.list.adapter.AssignmentListByTypeRecyclerAdapter +import com.instructure.student.features.assignments.list.adapter.AssignmentListFilter +import com.instructure.student.features.assignments.list.adapter.AssignmentListRecyclerAdapter +import com.instructure.student.fragment.ParentFragment import com.instructure.student.interfaces.AdapterToAssignmentsCallback import com.instructure.student.router.RouteMatcher import com.instructure.student.util.StudentPrefs +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import javax.inject.Inject @com.google.android.material.badge.ExperimentalBadgeUtils @ScreenView(SCREEN_VIEW_ASSIGNMENT_LIST) @PageView(url = "{canvasContext}/assignments") +@AndroidEntryPoint class AssignmentListFragment : ParentFragment(), Bookmarkable { + @Inject + lateinit var repository: AssignmentListRepository + private val binding by viewBinding(AssignmentListLayoutBinding::bind) private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -105,7 +112,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } override fun onRowClicked(assignment: Assignment, position: Int, isOpenDetail: Boolean) { - RouteMatcher.route(requireContext(), AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) + RouteMatcher.route(requireActivity(), AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) } override fun onRefreshFinished() { @@ -168,9 +175,21 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { private fun createRecyclerAdapter(): AssignmentListRecyclerAdapter { return if (sortOrder == AssignmentsSortOrder.SORT_BY_TIME) { - AssignmentListByDateRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback, filter = filter) + AssignmentListByDateRecyclerAdapter( + requireContext(), + canvasContext, + adapterToAssignmentsCallback, + filter = filter, + repository = repository + ) } else { - AssignmentListByTypeRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback, filter = filter) + AssignmentListByTypeRecyclerAdapter( + requireContext(), + canvasContext, + adapterToAssignmentsCallback, + filter = filter, + repository = repository + ) } } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListRepository.kt new file mode 100644 index 0000000000..9c650b05eb --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListRepository.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.list + +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.assignments.list.datasource.AssignmentListDataSource +import com.instructure.student.features.assignments.list.datasource.AssignmentListLocalDataSource +import com.instructure.student.features.assignments.list.datasource.AssignmentListNetworkDataSource + +class AssignmentListRepository( + localDataSource: AssignmentListLocalDataSource, + networkDataSource: AssignmentListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List { + return dataSource().getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId, + gradingPeriodId, + scopeToStudent, + forceNetwork + ) + } + + suspend fun getAssignmentGroupsWithAssignments( + courseId: Long, + isRefresh: Boolean + ): List { + return dataSource().getAssignmentGroupsWithAssignments(courseId, isRefresh) + } + + suspend fun getGradingPeriodsForCourse( + courseId: Long, + isRefresh: Boolean + ): List { + return dataSource().getGradingPeriodsForCourse(courseId, isRefresh) + } + + suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course? { + return dataSource().getCourseWithGrade(courseId, forceNetwork) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListByDateRecyclerAdapter.kt similarity index 92% rename from apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListByDateRecyclerAdapter.kt index 51dc99259b..ecb8318236 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListByDateRecyclerAdapter.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.adapter.assignment +package com.instructure.student.features.assignments.list.adapter import android.content.Context import com.instructure.canvasapi2.models.Assignment @@ -26,6 +26,7 @@ import com.instructure.canvasapi2.utils.toDate import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types import com.instructure.student.R +import com.instructure.student.features.assignments.list.AssignmentListRepository import com.instructure.student.interfaces.AdapterToAssignmentsCallback import java.util.* @@ -35,12 +36,13 @@ private const val HEADER_POSITION_UNDATED = 2 private const val HEADER_POSITION_PAST = 3 class AssignmentListByDateRecyclerAdapter( - context: Context, - canvasContext: CanvasContext, - adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false, - filter: AssignmentListFilter = AssignmentListFilter.ALL -) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting, filter) { + context: Context, + canvasContext: CanvasContext, + adapterToAssignmentsCallback: AdapterToAssignmentsCallback, + isTesting: Boolean = false, + filter: AssignmentListFilter = AssignmentListFilter.ALL, + repository: AssignmentListRepository +) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting, filter, repository) { private val overdue = AssignmentGroup(name = context.getString(R.string.overdueAssignments), position = HEADER_POSITION_OVERDUE) private val upcoming = AssignmentGroup(name = context.getString(R.string.upcomingAssignments), position = HEADER_POSITION_UPCOMING) diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListByTypeRecyclerAdapter.kt similarity index 85% rename from apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListByTypeRecyclerAdapter.kt index 15f21199ee..18ae377569 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListByTypeRecyclerAdapter.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.adapter.assignment +package com.instructure.student.features.assignments.list.adapter import android.content.Context import com.instructure.canvasapi2.models.Assignment @@ -23,16 +23,18 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.filterWithQuery import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types +import com.instructure.student.features.assignments.list.AssignmentListRepository import com.instructure.student.interfaces.AdapterToAssignmentsCallback import java.util.* class AssignmentListByTypeRecyclerAdapter( - context: Context, - canvasContext: CanvasContext, - adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false, - filter: AssignmentListFilter = AssignmentListFilter.ALL -) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting, filter) { + context: Context, + canvasContext: CanvasContext, + adapterToAssignmentsCallback: AdapterToAssignmentsCallback, + isTesting: Boolean = false, + filter: AssignmentListFilter = AssignmentListFilter.ALL, + repository: AssignmentListRepository +) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting, filter, repository) { override fun populateData() { assignmentGroups diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt similarity index 77% rename from apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt index ff503444bc..0dcb1d462a 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt @@ -14,20 +14,20 @@ * along with this program. If not, see . * */ -package com.instructure.student.adapter.assignment +package com.instructure.student.features.assignments.list.adapter import android.content.Context import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.StatusCallback -import com.instructure.canvasapi2.managers.AssignmentManager -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.utils.ApiType -import com.instructure.canvasapi2.utils.LinkHeaders import com.instructure.canvasapi2.utils.Logger import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.pandarecycler.util.GroupSortedList @@ -35,30 +35,32 @@ import com.instructure.pandarecycler.util.Types import com.instructure.pandautils.utils.backgroundColor import com.instructure.student.R import com.instructure.student.adapter.ExpandableRecyclerAdapter +import com.instructure.student.features.assignments.list.AssignmentListRepository import com.instructure.student.holders.AssignmentViewHolder import com.instructure.student.holders.EmptyViewHolder import com.instructure.student.holders.ExpandableViewHolder import com.instructure.student.interfaces.AdapterToAssignmentsCallback import com.instructure.student.interfaces.GradingPeriodsCallback -import retrofit2.Call -import retrofit2.Response -abstract class AssignmentListRecyclerAdapter ( - context: Context, - private val canvasContext: CanvasContext, - private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false, - filter: AssignmentListFilter = AssignmentListFilter.ALL +abstract class AssignmentListRecyclerAdapter( + context: Context, + private val canvasContext: CanvasContext, + private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback, + isTesting: Boolean = false, + filter: AssignmentListFilter = AssignmentListFilter.ALL, + private val repository: AssignmentListRepository ) : ExpandableRecyclerAdapter( - context, - AssignmentGroup::class.java, - Assignment::class.java + context, + AssignmentGroup::class.java, + Assignment::class.java ), GradingPeriodsCallback { - private var assignmentGroupCallback: StatusCallback>? = null override var currentGradingPeriod: GradingPeriod? = null - private var apiJob: WeaveJob? = null + + private var assignmentGroupsJob: WeaveJob? = null + private var gradingPeriodJob: WeaveJob? = null private var courseJob: WeaveJob? = null + protected var assignmentGroups: List = emptyList() private var restrictQuantitativeData = false private var gradingSchemes = listOf() @@ -90,28 +92,6 @@ abstract class AssignmentListRecyclerAdapter ( if (!isTesting) loadData() } - override fun setupCallbacks() { - assignmentGroupCallback = object : StatusCallback>() { - - override fun onResponse(response: Response>, linkHeaders: LinkHeaders, type: ApiType) { - assignmentGroups = response.body()!! - populateData() - adapterToAssignmentsCallback.onRefreshFinished() - adapterToAssignmentsCallback.assignmentLoadingFinished() - } - - override fun onFail(call: Call>?, error: Throwable, response: Response<*>?) { - adapterToAssignmentsCallback.assignmentLoadingFinished() - } - - override fun onFinished(type: ApiType) { - this@AssignmentListRecyclerAdapter.onCallbackFinished(type) - } - } - - - } - override fun createViewHolder(v: View, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { Types.TYPE_HEADER -> ExpandableViewHolder(v) @@ -134,7 +114,7 @@ abstract class AssignmentListRecyclerAdapter ( val course = canvasContext as Course courseJob = tryWeave { - val detailedCourse = CourseManager.getCourseWithGradeAsync(canvasContext.id, isRefresh).await().dataOrNull + val detailedCourse = repository.getCourseWithGrade(canvasContext.id, isRefresh) restrictQuantitativeData = detailedCourse?.settings?.restrictQuantitativeData ?: false gradingSchemes = detailedCourse?.gradingScheme ?: emptyList() loadAssignmentsData(course) @@ -178,10 +158,9 @@ abstract class AssignmentListRecyclerAdapter ( } private fun fetchGradingPeriods(courseId: Long) { - apiJob = tryWeave { - val periods = awaitApi { - CourseManager.getGradingPeriodsForCourse(it, courseId, isRefresh) - }.gradingPeriodList + gradingPeriodJob?.cancel() + gradingPeriodJob = tryWeave { + val periods = repository.getGradingPeriodsForCourse(courseId, isRefresh) adapterToAssignmentsCallback.gradingPeriodsFetched(periods) } catch { adapterToAssignmentsCallback.gradingPeriodsFetched(emptyList()) @@ -220,24 +199,46 @@ abstract class AssignmentListRecyclerAdapter ( ) } - override fun loadAssignmentsForGradingPeriod(gradingPeriodID: Long, refreshFirst: Boolean) { + override fun loadAssignmentsForGradingPeriod(gradingPeriodId: Long, refreshFirst: Boolean) { /*Logic regarding MGP is similar here as it is in both assignment recycler adapters, if changes are made here, check if they are needed in the other recycler adapters.*/ if (refreshFirst) resetData() // Scope assignments if its for a student val scopeToStudent = (canvasContext as Course).isStudent - AssignmentManager.getAssignmentGroupsWithAssignmentsForGradingPeriod( + + assignmentGroupsJob?.cancel() + assignmentGroupsJob = tryWeave { + val groups = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod( canvasContext.id, - gradingPeriodID, + gradingPeriodId, scopeToStudent, - isRefresh, - assignmentGroupCallback!! - ) + isRefresh + ) + onAssignmentGroupsFetched(groups) + } catch { + adapterToAssignmentsCallback.assignmentLoadingFinished() + onCallbackFinished(null) + } } override fun loadAssignment() { - AssignmentManager.getAssignmentGroupsWithAssignments(canvasContext.id, isRefresh, assignmentGroupCallback!!) + assignmentGroupsJob?.cancel() + assignmentGroupsJob = tryWeave { + val groups = repository.getAssignmentGroupsWithAssignments(canvasContext.id, isRefresh) + onAssignmentGroupsFetched(groups) + } catch { + adapterToAssignmentsCallback.assignmentLoadingFinished() + onCallbackFinished(null) + } + } + + private fun onAssignmentGroupsFetched(groups: List) { + assignmentGroups = groups + populateData() + adapterToAssignmentsCallback.onRefreshFinished() + adapterToAssignmentsCallback.assignmentLoadingFinished() + onCallbackFinished(null) } protected abstract fun populateData() @@ -256,7 +257,8 @@ abstract class AssignmentListRecyclerAdapter ( override fun cancel() { super.cancel() - apiJob?.cancel() + assignmentGroupsJob?.cancel() + gradingPeriodJob?.cancel() courseJob?.cancel() } } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListDataSource.kt new file mode 100644 index 0000000000..feb2ba88a6 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListDataSource.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.list.datasource + +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod + +interface AssignmentListDataSource { + + suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List + + suspend fun getAssignmentGroupsWithAssignments( + courseId: Long, + forceNetwork: Boolean + ): List + + suspend fun getGradingPeriodsForCourse( + courseId: Long, + forceNetwork: Boolean + ): List + + suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course? +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListLocalDataSource.kt new file mode 100644 index 0000000000..211a643fb1 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListLocalDataSource.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.list.datasource + +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade + +class AssignmentListLocalDataSource( + private val assignmentFacade: AssignmentFacade, + private val courseFacade: CourseFacade, + private val courseSettingsDao: CourseSettingsDao +) : AssignmentListDataSource { + + override suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List { + return assignmentFacade.getAssignmentGroupsWithAssignmentsForGradingPeriod(courseId, gradingPeriodId) + } + + override suspend fun getAssignmentGroupsWithAssignments(courseId: Long, forceNetwork: Boolean): List { + return assignmentFacade.getAssignmentGroupsWithAssignments(courseId) + } + + override suspend fun getGradingPeriodsForCourse(courseId: Long, forceNetwork: Boolean): List { + return courseFacade.getGradingPeriodsByCourseId(courseId) + } + + override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course? { + return courseFacade.getCourseById(courseId) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListNetworkDataSource.kt new file mode 100644 index 0000000000..5799a15e35 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/datasource/AssignmentListNetworkDataSource.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignments.list.datasource + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.canvasapi2.utils.depaginate + +class AssignmentListNetworkDataSource( + private val assignmentApi: AssignmentAPI.AssignmentInterface, + private val courseApi: CourseAPI.CoursesInterface +) : AssignmentListDataSource { + + override suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return assignmentApi.getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + courseId = courseId, + gradingPeriodId = gradingPeriodId, + scopeToStudent = scopeToStudent, + restParams = params + ).depaginate { + assignmentApi.getNextPageAssignmentGroupListWithAssignmentsForGradingPeriod(it, params) + }.dataOrThrow + } + + override suspend fun getAssignmentGroupsWithAssignments(courseId: Long, forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return assignmentApi.getFirstPageAssignmentGroupListWithAssignments(courseId, params).depaginate { + assignmentApi.getNextPageAssignmentGroupListWithAssignments(it, params) + }.dataOrThrow + } + + override suspend fun getGradingPeriodsForCourse(courseId: Long, forceNetwork: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return courseApi.getGradingPeriodsForCourse(courseId, params).dataOrThrow.gradingPeriodList + } + + override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course? { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return courseApi.getCourseWithGrade(courseId, params).dataOrNull + } +} diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt similarity index 77% rename from apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt index 693f665549..68c048d916 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CourseBrowserFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserFragment.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.fragment +package com.instructure.student.features.coursebrowser import android.animation.ObjectAnimator import android.content.res.Configuration @@ -26,13 +26,10 @@ import android.view.ViewGroup import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import com.google.android.material.appbar.AppBarLayout -import com.instructure.canvasapi2.managers.PageManager -import com.instructure.canvasapi2.managers.TabManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.isValid import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.weave.StatusCallbackError -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.interactions.FragmentInteractions @@ -46,18 +43,25 @@ import com.instructure.student.R import com.instructure.student.adapter.CourseBrowserAdapter import com.instructure.student.databinding.FragmentCourseBrowserBinding import com.instructure.student.events.CourseColorOverlayToggledEvent +import com.instructure.student.features.pages.details.PageDetailsFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const import com.instructure.student.util.DisableableAppBarLayoutBehavior import com.instructure.student.util.StudentPrefs import com.instructure.student.util.TabHelper +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import javax.inject.Inject @ScreenView(SCREEN_VIEW_COURSE_BROWSER) @PageView(url = "{canvasContext}") -class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnOffsetChangedListener { +@AndroidEntryPoint +class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnOffsetChangedListener { + + @Inject + lateinit var repository: CourseBrowserRepository private val binding by viewBinding(FragmentCourseBrowserBinding::bind) @@ -70,7 +74,7 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO //region Fragment Lifecycle Overrides override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(R.layout.fragment_course_browser, container, false) + inflater.inflate(R.layout.fragment_course_browser, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { super.onViewCreated(view, savedInstanceState) @@ -147,7 +151,13 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // Set course image again after orientation change to ensure correct scale/crop - (canvasContext as? Course)?.let { binding.courseImage.setCourseImage(it, it.backgroundColor, !StudentPrefs.hideCourseColorOverlay) } + (canvasContext as? Course)?.let { + binding.courseImage.setCourseImage( + it, + it.backgroundColor, + !StudentPrefs.hideCourseColorOverlay + ) + } } override fun onDestroyView() { @@ -159,8 +169,16 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO //region Fragment Interaction Overrides override fun applyTheme() { - ViewStyler.colorToolbarIconsAndText(requireActivity(), binding.noOverlayToolbar, requireContext().getColor(R.color.white)) - ViewStyler.colorToolbarIconsAndText(requireActivity(), binding.overlayToolbar, requireContext().getColor(R.color.white)) + ViewStyler.colorToolbarIconsAndText( + requireActivity(), + binding.noOverlayToolbar, + requireContext().getColor(R.color.white) + ) + ViewStyler.colorToolbarIconsAndText( + requireActivity(), + binding.overlayToolbar, + requireContext().getColor(R.color.white) + ) ViewStyler.setStatusBarDark(requireActivity(), canvasContext.backgroundColor) } @@ -176,14 +194,15 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO // We don't want to list external tools that are hidden var homePageTitle: String? = null - val isHomeAPage = if (canvasContext is Course) TabHelper.isHomeTabAPage(canvasContext as Course) else false // Courses are the only CanvasContext that have settable home pages + val isHomeAPage = + if (canvasContext is Course) TabHelper.isHomeTabAPage(canvasContext as Course) else false // Courses are the only CanvasContext that have settable home pages - if(isHomeAPage) { - val homePage = awaitApi { PageManager.getFrontPage(canvasContext, isRefresh, it) } - homePageTitle = homePage.title + if (isHomeAPage) { + val homePage = repository.getFrontPage(canvasContext, isRefresh) + homePageTitle = homePage?.title } - val tabs = awaitApi> { TabManager.getTabs(canvasContext, it, isRefresh) }.filter { !(it.isExternal && it.isHidden) } + val tabs = repository.getTabs(canvasContext, isRefresh) // Finds the home tab so we can reorder them if necessary val sortedTabs = tabs.toMutableList() @@ -198,7 +217,10 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO } // If the home tab is a Page and we clicked it lets route directly there. - RouteMatcher.route(requireActivity(), PageDetailsFragment.makeRoute(canvasContext, Page.FRONT_PAGE_NAME).apply { ignoreDebounce = true }) + RouteMatcher.route( + requireActivity(), + PageDetailsFragment.makeRoute(canvasContext, Page.FRONT_PAGE_NAME) + .apply { ignoreDebounce = true }) } else { val route = TabHelper.getRouteByTabId(tab, canvasContext)?.apply { ignoreDebounce = true } RouteMatcher.route(requireActivity(), route) @@ -227,10 +249,21 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO val percentage = Math.abs(verticalOffset).div(appBarLayout?.totalScrollRange?.toFloat() ?: 1F) if (percentage <= 0.3F) { - val toolbarAnimation = if(binding.courseBrowserHeader.courseBrowserHeader==null) null else ObjectAnimator.ofFloat(binding.courseBrowserHeader.courseBrowserHeader, View.ALPHA, binding.courseBrowserHeader.courseBrowserHeader.alpha, 0F) + val toolbarAnimation = + if (binding.courseBrowserHeader.courseBrowserHeader == null) null else ObjectAnimator.ofFloat( + binding.courseBrowserHeader.courseBrowserHeader, + View.ALPHA, + binding.courseBrowserHeader.courseBrowserHeader.alpha, + 0F + ) val titleAnimation = ObjectAnimator.ofFloat(binding.courseBrowserTitle, View.ALPHA, binding.courseBrowserTitle.alpha, 1F) - val subtitleAnimation = ObjectAnimator.ofFloat(binding.courseBrowserSubtitle, View.ALPHA, binding.courseBrowserSubtitle.alpha, 0.8F) + val subtitleAnimation = ObjectAnimator.ofFloat( + binding.courseBrowserSubtitle, + View.ALPHA, + binding.courseBrowserSubtitle.alpha, + 0.8F + ) toolbarAnimation?.setAutoCancel(true) titleAnimation?.setAutoCancel(true) @@ -249,10 +282,21 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO subtitleAnimation?.start() } else if (percentage > 0.7F) { - val toolbarAnimation = if(binding.courseBrowserHeader.courseBrowserHeader==null) null else ObjectAnimator.ofFloat(binding.courseBrowserHeader.courseBrowserHeader, View.ALPHA, binding.courseBrowserHeader.courseBrowserHeader.alpha, 1F) + val toolbarAnimation = + if (binding.courseBrowserHeader.courseBrowserHeader == null) null else ObjectAnimator.ofFloat( + binding.courseBrowserHeader.courseBrowserHeader, + View.ALPHA, + binding.courseBrowserHeader.courseBrowserHeader.alpha, + 1F + ) val titleAnimation = ObjectAnimator.ofFloat(binding.courseBrowserTitle, View.ALPHA, binding.courseBrowserTitle.alpha, 0F) - val subtitleAnimation = ObjectAnimator.ofFloat(binding.courseBrowserSubtitle, View.ALPHA, binding.courseBrowserSubtitle.alpha, 0F) + val subtitleAnimation = ObjectAnimator.ofFloat( + binding.courseBrowserSubtitle, + View.ALPHA, + binding.courseBrowserSubtitle.alpha, + 0F + ) toolbarAnimation?.setAutoCancel(true) titleAnimation?.setAutoCancel(true) @@ -274,9 +318,9 @@ class CourseBrowserFragment : Fragment(), FragmentInteractions, AppBarLayout.OnO companion object { fun newInstance(route: Route) = - if (validateRoute(route)) CourseBrowserFragment().apply { - arguments = route.canvasContext!!.makeBundle(route.arguments) - } else null + if (validateRoute(route)) CourseBrowserFragment().apply { + arguments = route.canvasContext!!.makeBundle(route.arguments) + } else null private fun validateRoute(route: Route) = route.canvasContext != null diff --git a/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserRepository.kt b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserRepository.kt new file mode 100644 index 0000000000..65bbb39f72 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/CourseBrowserRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.coursebrowser + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserDataSource +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserLocalDataSource +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserNetworkDataSource + +class CourseBrowserRepository( + networkDataSource: CourseBrowserNetworkDataSource, + localDataSource: CourseBrowserLocalDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val tabs = dataSource().getTabs(canvasContext, forceNetwork) + return tabs.filter { !(it.isExternal && it.isHidden) } + } + + suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): Page? { + return dataSource().getFrontPage(canvasContext, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserDataSource.kt new file mode 100644 index 0000000000..f10956e81e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserDataSource.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.coursebrowser.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab + +interface CourseBrowserDataSource { + + suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List + + suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): Page? +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserLocalDataSource.kt new file mode 100644 index 0000000000..9b73b320bb --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserLocalDataSource.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.coursebrowser.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.utils.orDefault + +class CourseBrowserLocalDataSource( + private val tabDao: TabDao, + private val pageFacade: PageFacade, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val fileSyncSettingsDao: FileSyncSettingsDao +) : CourseBrowserDataSource { + override suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val courseSyncSettings = courseSyncSettingsDao.findById(canvasContext.id) + var syncedTabs = courseSyncSettings?.tabs + val filesSynced = courseSyncSettings?.fullFileSync == true || fileSyncSettingsDao.findByCourseId(canvasContext.id).isNotEmpty() + + if (filesSynced) { + syncedTabs = syncedTabs?.plus(Pair(Tab.FILES_ID, true)) + } + + return tabDao.findByCourseId(canvasContext.id).map { + it.toApiModel().copy(enabled = syncedTabs?.get(it.id).orDefault()) + } + } + + override suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): Page? { + return pageFacade.getFrontPage(canvasContext.id) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserNetworkDataSource.kt new file mode 100644 index 0000000000..a9d7a906b6 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/coursebrowser/datasource/CourseBrowserNetworkDataSource.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.coursebrowser.datasource + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab + +class CourseBrowserNetworkDataSource( + private val tabApi: TabAPI.TabsInterface, + private val pageApi: PageAPI.PagesInterface +) : CourseBrowserDataSource { + override suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return tabApi.getTabs(canvasContext.id, canvasContext.type.apiString, params).dataOrThrow + } + + override suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): Page? { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return pageApi.getFrontPage(canvasContext.apiContext(), canvasContext.id, params).dataOrNull + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardDataSource.kt new file mode 100644 index 0000000000..307634ec57 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardDataSource.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Group + +interface DashboardDataSource { + + suspend fun getCourses(forceNetwork: Boolean): List + + suspend fun getGroups(forceNetwork: Boolean): List + + suspend fun getDashboardCards(forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardLocalDataSource.kt new file mode 100644 index 0000000000..d6f4fd1f7d --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardLocalDataSource.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.daos.DashboardCardDao +import com.instructure.pandautils.room.offline.entities.DashboardCardEntity +import com.instructure.pandautils.room.offline.facade.CourseFacade + +class DashboardLocalDataSource( + private val courseFacade: CourseFacade, + private val dashboardCardDao: DashboardCardDao +) : DashboardDataSource { + + override suspend fun getCourses(forceNetwork: Boolean): List { + return courseFacade.getAllCourses() + } + + override suspend fun getGroups(forceNetwork: Boolean): List { + return emptyList() + } + + override suspend fun getDashboardCards(forceNetwork: Boolean): List { + return dashboardCardDao.findAll().map { it.toApiModel() } + } + + suspend fun saveDashboardCards(dashboardCards: List) { + dashboardCardDao.updateEntities(dashboardCards.map { DashboardCardEntity(it) }) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt new file mode 100644 index 0000000000..b60a138f73 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardNetworkDataSource.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.depaginate + +class DashboardNetworkDataSource( + private val courseApi: CourseAPI.CoursesInterface, + private val groupApi: GroupAPI.GroupInterface, + private val apiPrefs: ApiPrefs +): DashboardDataSource { + + override suspend fun getCourses(forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + val coursesResult = if (apiPrefs.isStudentView) { + courseApi.getFirstPageCoursesTeacher(params).depaginate { nextUrl -> courseApi.next(nextUrl, params) } + } else { + courseApi.getFirstPageCourses(params).depaginate { nextUrl -> courseApi.next(nextUrl, params) } + } + + return coursesResult.dataOrNull.orEmpty() + } + + override suspend fun getGroups(forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + val groupsResult = groupApi.getFirstPageGroups(params) + .depaginate { nextUrl -> groupApi.getNextPageGroups(nextUrl, params) } + + return groupsResult.dataOrNull ?: emptyList() + } + + override suspend fun getDashboardCards(forceNetwork: Boolean): List { + return courseApi.getDashboardCourses(RestParams(isForceReadFromNetwork = forceNetwork)).dataOrNull.orEmpty() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt new file mode 100644 index 0000000000..4d85ff6b22 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/DashboardRepository.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class DashboardRepository( + private val localDataSource: DashboardLocalDataSource, + networkDataSource: DashboardNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val courseDao: CourseDao +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getCourses(forceNetwork: Boolean): List { + return dataSource().getCourses(forceNetwork) + } + + suspend fun getGroups(forceNetwork: Boolean): List { + return dataSource().getGroups(forceNetwork) + } + + suspend fun getDashboardCourses(forceNetwork: Boolean): List { + val dashboardCards = dataSource().getDashboardCards(forceNetwork).sortedBy { it.position } + if (isOnline() && isOfflineEnabled()) { + localDataSource.saveDashboardCards(dashboardCards) + } + return dashboardCards + } + + suspend fun getSyncedCourseIds(): Set { + if (!isOfflineEnabled()) return emptySet() + + val courseSyncSettings = courseSyncSettingsDao.findAll() + val syncedCourseIds = courseSyncSettings + .filter { it.anySyncEnabled } + .map { it.courseId } + .toSet() + + val syncedCourses = courseDao.findByIds(syncedCourseIds) + return syncedCourses.map { it.id }.toSet() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepository.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepository.kt index 972e114dad..66796031da 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepository.kt @@ -17,8 +17,6 @@ package com.instructure.student.features.dashboard.edit -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.hasActiveEnrollment @@ -26,30 +24,46 @@ import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.isPublished import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository -import kotlinx.coroutines.awaitAll +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardDataSource +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardLocalDataSource +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardNetworkDataSource class StudentEditDashboardRepository( - val courseManager: CourseManager, - val groupManager: GroupManager -) : EditDashboardRepository { - - override suspend fun getCurses(): List> { - val (currentCoursesDeferred, pastCoursesDeferred, futureCoursesDeferred) = listOf( - courseManager.getCoursesByEnrollmentStateAsync("active", true), - courseManager.getCoursesByEnrollmentStateAsync("completed", true), - courseManager.getCoursesByEnrollmentStateAsync("invited_or_pending", true) - ).awaitAll() - - val currentCourses = currentCoursesDeferred.dataOrThrow - val pastCourses = pastCoursesDeferred.dataOrThrow - val futureCourses = futureCoursesDeferred.dataOrThrow - - return listOf(currentCourses, pastCourses, futureCourses) + localDataSource: StudentEditDashboardLocalDataSource, + networkDataSource: StudentEditDashboardNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val courseDao: CourseDao +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider), EditDashboardRepository { + + override suspend fun getCourses(): List> { + return dataSource().getCourses() } - override suspend fun getGroups(): List = groupManager.getAllGroupsAsync(true).await().dataOrThrow + override suspend fun getGroups(): List = dataSource().getGroups() override fun isOpenable(course: Course) = course.isNotDeleted() && course.isPublished() override fun isFavoriteable(course: Course) = course.isValidTerm() && course.isNotDeleted() && course.isPublished() && course.hasActiveEnrollment() + + override suspend fun getSyncedCourseIds(): Set { + if (!isOfflineEnabled()) return emptySet() + + val courseSyncSettings = courseSyncSettingsDao.findAll() + val syncedCourseIds = courseSyncSettings + .filter { it.anySyncEnabled } + .map { it.courseId } + .toSet() + + val syncedCourses = courseDao.findByIds(syncedCourseIds) + return syncedCourses.map { it.id }.toSet() + } + + override suspend fun offlineEnabled(): Boolean = isOfflineEnabled() } diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRouter.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRouter.kt index e6d77b0061..e8cddb81d0 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRouter.kt @@ -19,7 +19,7 @@ package com.instructure.student.features.dashboard.edit import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter -import com.instructure.student.fragment.CourseBrowserFragment +import com.instructure.student.features.coursebrowser.CourseBrowserFragment import com.instructure.student.router.RouteMatcher class StudentEditDashboardRouter(private val activity: FragmentActivity) : EditDashboardRouter { diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardDataSource.kt new file mode 100644 index 0000000000..2960d69c3d --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardDataSource.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard.edit.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group + +interface StudentEditDashboardDataSource { + + suspend fun getCourses(): List> + suspend fun getGroups(): List +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSource.kt new file mode 100644 index 0000000000..91d4c26479 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSource.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard.edit.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.daos.EditDashboardItemDao +import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity +import com.instructure.pandautils.room.offline.entities.EnrollmentState +import com.instructure.pandautils.room.offline.facade.CourseFacade + +class StudentEditDashboardLocalDataSource( + private val courseFacade: CourseFacade, + private val editDashboardItemDao: EditDashboardItemDao +) : StudentEditDashboardDataSource { + + override suspend fun getCourses(): List> { + val courseMapper: suspend (EditDashboardItemEntity) -> Course = { + courseFacade.getCourseById(it.courseId)?.copy(isFavorite = it.isFavorite) ?: it.toCourse() + } + + val currentCourses = editDashboardItemDao.findByEnrollmentState(EnrollmentState.CURRENT).map { courseMapper(it) } + val pastCourses = editDashboardItemDao.findByEnrollmentState(EnrollmentState.PAST).map { courseMapper(it) } + val futureCourses = editDashboardItemDao.findByEnrollmentState(EnrollmentState.FUTURE).map { courseMapper(it) } + + return listOf(currentCourses, pastCourses, futureCourses) + } + + override suspend fun getGroups(): List { + return emptyList() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetworkDataSource.kt new file mode 100644 index 0000000000..ae62f7f9e8 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetworkDataSource.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard.edit.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.depaginate +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +class StudentEditDashboardNetworkDataSource( + private val courseApi: CourseAPI.CoursesInterface, + private val groupApi: GroupAPI.GroupInterface +) : StudentEditDashboardDataSource { + + override suspend fun getCourses(): List> { + val params = RestParams(isForceReadFromNetwork = true, usePerPageQueryParam = true) + + return coroutineScope { + val currentCourses = async { courseApi.firstPageCoursesByEnrollmentState("active", params) + .depaginate { nextUrl -> courseApi.next(nextUrl, params) }.dataOrThrow } + val pastCourses = async { courseApi.firstPageCoursesByEnrollmentState("completed", params) + .depaginate { nextUrl -> courseApi.next(nextUrl, params) }.dataOrThrow } + val futureCourses = async { courseApi.firstPageCoursesByEnrollmentState("invited_or_pending", params) + .depaginate { nextUrl -> courseApi.next(nextUrl, params) }.dataOrThrow.filter { it.workflowState != Course.WorkflowState.UNPUBLISHED } } + + return@coroutineScope listOf(currentCourses, pastCourses, futureCourses).awaitAll() + } + } + + override suspend fun getGroups(): List { + val params = RestParams(isForceReadFromNetwork = true, usePerPageQueryParam = true) + + return groupApi.getFirstPageGroups(params) + .depaginate { nextUrl -> groupApi.getNextPageGroups(nextUrl, params) } + .dataOrNull.orEmpty() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/dashboard/notifications/StudentDashboardRouter.kt b/apps/student/src/main/java/com/instructure/student/features/dashboard/notifications/StudentDashboardRouter.kt index 44d5f46f5c..00180b2bd2 100644 --- a/apps/student/src/main/java/com/instructure/student/features/dashboard/notifications/StudentDashboardRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/dashboard/notifications/StudentDashboardRouter.kt @@ -19,9 +19,10 @@ package com.instructure.student.features.dashboard.notifications import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter -import com.instructure.student.fragment.FileListFragment +import com.instructure.pandautils.features.offline.sync.progress.SyncProgressFragment +import com.instructure.student.features.files.list.FileListFragment import com.instructure.student.fragment.InternalWebviewFragment -import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment import com.instructure.student.router.RouteMatcher class StudentDashboardRouter(private val activity: FragmentActivity) : DashboardRouter { @@ -42,7 +43,7 @@ class StudentDashboardRouter(private val activity: FragmentActivity) : Dashboard override fun routeToSubmissionDetails(canvasContext: CanvasContext, assignmentId: Long, attemptId: Long) { RouteMatcher.route( activity, - SubmissionDetailsFragment.makeRoute(canvasContext, assignmentId, initialSelectedSubmissionAttempt = attemptId) + SubmissionDetailsRepositoryFragment.makeRoute(canvasContext, assignmentId, initialSelectedSubmissionAttempt = attemptId) ) } @@ -52,4 +53,11 @@ class StudentDashboardRouter(private val activity: FragmentActivity) : Dashboard FileListFragment.makeRoute(canvasContext, folderId) ) } + + override fun routeToSyncProgress() { + RouteMatcher.route( + activity, + SyncProgressFragment.makeRoute() + ) + } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt similarity index 82% rename from apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt index f3b43fbb6c..75279fe0b8 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.fragment +package com.instructure.student.features.discussion.details import android.annotation.SuppressLint import android.os.Bundle @@ -29,13 +29,10 @@ import android.webkit.WebView import android.widget.ScrollView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.instructure.canvasapi2.StatusCallback -import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.managers.DiscussionManager -import com.instructure.canvasapi2.managers.DiscussionManager.deleteDiscussionEntry -import com.instructure.canvasapi2.managers.GroupManager -import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.BeforePageView @@ -63,30 +60,35 @@ import com.instructure.student.events.DiscussionTopicHeaderEvent import com.instructure.student.events.DiscussionUpdatedEvent import com.instructure.student.events.ModuleUpdatedEvent import com.instructure.student.events.post +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment +import com.instructure.student.fragment.DiscussionsReplyFragment +import com.instructure.student.fragment.DiscussionsUpdateFragment +import com.instructure.student.fragment.InternalWebviewFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import retrofit2.Response import java.net.URLDecoder import java.util.* import java.util.regex.Pattern +import javax.inject.Inject @ScreenView(SCREEN_VIEW_DISCUSSION_DETAILS) @PageView(url = "{canvasContext}/discussion_topics/{topicId}") +@AndroidEntryPoint class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { private val binding by viewBinding(FragmentDiscussionsDetailsBinding::bind) - // Weave jobs - private var sessionAuthJob: Job? = null - private var discussionMarkAsReadJob: Job? = null - private var discussionLikeJob: Job? = null - private var discussionsLoadingJob: WeaveJob? = null - private var loadHeaderHtmlJob: Job? = null - private var loadRepliesHtmlJob: Job? = null + + @Inject + lateinit var repository: DiscussionDetailsRepository // Bundle args private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -103,6 +105,8 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { private var scrollPosition: Int = 0 private var authenticatedSessionURL: String? = null + private var markAsReadJob: Job? = null + //region Analytics @Suppress("unused") @PageViewUrlParam("topicId") @@ -122,7 +126,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { populateDiscussionData() binding.swipeRefreshLayout.setOnRefreshListener { authenticatedSessionURL = null - populateDiscussionData(true) + populateDiscussionData() // Send out bus events to trigger a refresh for discussion list DiscussionUpdatedEvent(discussionTopicHeader, javaClass.simpleName).post() } @@ -164,15 +168,6 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { EventBus.getDefault().unregister(this) } - override fun onDestroyView() { - super.onDestroyView() - sessionAuthJob?.cancel() - discussionMarkAsReadJob?.cancel() - discussionLikeJob?.cancel() - discussionsLoadingJob?.cancel() - loadHeaderHtmlJob?.cancel() - loadRepliesHtmlJob?.cancel() - } //endregion //region Fragment Interaction Overrides @@ -202,27 +197,44 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { //region Discussion Actions private fun viewAttachments(remoteFiles: List) { - // Only one file can be attached to a discussion - val remoteFile = remoteFiles.firstOrNull() ?: return - - // Show lock message if file is locked - if (remoteFile.lockedForUser) { - if (remoteFile.lockExplanation.isValid()) { - Snackbar.make(requireView(), remoteFile.lockExplanation!!, Snackbar.LENGTH_SHORT).show() - } else { - Snackbar.make(requireView(), R.string.fileCurrentlyLocked, Snackbar.LENGTH_SHORT).show() + if (repository.isOnline()) { + // Only one file can be attached to a discussion + val remoteFile = remoteFiles.firstOrNull() ?: return + + // Show lock message if file is locked + if (remoteFile.lockedForUser) { + if (remoteFile.lockExplanation.isValid()) { + Snackbar.make( + requireView(), + remoteFile.lockExplanation!!, + Snackbar.LENGTH_SHORT + ).show() + } else { + Snackbar.make( + requireView(), + R.string.fileCurrentlyLocked, + Snackbar.LENGTH_SHORT + ).show() + } } - } - // Show attachment - val attachment = remoteFile.mapToAttachment() - openMedia(attachment.contentType, attachment.url, attachment.filename, canvasContext) + // Show attachment + val attachment = remoteFile.mapToAttachment() + openMedia(attachment.contentType, attachment.url, attachment.filename, canvasContext) + } else { + NoInternetConnectionDialog.show(requireFragmentManager()) + } } private fun showReplyView(discussionEntryId: Long) { - if (APIHelper.hasNetworkConnection()) { + if (repository.isOnline()) { scrollPosition = binding.discussionsScrollView.scrollY - val route = DiscussionsReplyFragment.makeRoute(canvasContext, discussionTopicHeader.id, discussionEntryId, discussionTopicHeader.permissions!!.attach) + val route = DiscussionsReplyFragment.makeRoute( + canvasContext, + discussionTopicHeader.id, + discussionEntryId, + discussionTopicHeader.permissions!!.attach + ) RouteMatcher.route(requireActivity(), route) } else { NoInternetConnectionDialog.show(requireFragmentManager()) @@ -231,29 +243,23 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { @Suppress("EXPERIMENTAL_FEATURE_WARNING") fun markAsRead(discussionEntryIds: List) { - if (discussionMarkAsReadJob?.isActive == true) return - discussionMarkAsReadJob = tryWeave { - val successfullyMarkedAsReadIds: MutableList = ArrayList(discussionEntryIds.size) - discussionEntryIds.forEach { entryId -> - val response = awaitApiResponse { DiscussionManager.markDiscussionTopicEntryRead(canvasContext, discussionTopicHeader.id, entryId, it) } - if (response.isSuccessful) { - successfullyMarkedAsReadIds.add(entryId) - discussionTopic?.let { - val entry = DiscussionUtils.findEntry(entryId, it.views) - entry?.unread = false - it.unreadEntriesMap.remove(entryId) - it.unreadEntries.remove(entryId) - if (discussionTopicHeader.unreadCount > 0) discussionTopicHeader.unreadCount -= 1 - } + if (markAsReadJob?.isActive == true) return + markAsReadJob = lifecycleScope.tryLaunch { + repository.markAsRead(canvasContext, discussionTopicHeader.id, discussionEntryIds).forEach { entryId -> + discussionTopic?.let { + val entry = DiscussionUtils.findEntry(entryId, it.views) + entry?.unread = false + it.unreadEntriesMap.remove(entryId) + it.unreadEntries.remove(entryId) + if (discussionTopicHeader.unreadCount > 0) discussionTopicHeader.unreadCount -= 1 } - } - successfullyMarkedAsReadIds.forEach { binding.discussionRepliesWebViewWrapper.post { // Posting lets this escape Weave's lifecycle, so use a null-safe call on the webview here - if (view != null) binding.discussionRepliesWebViewWrapper.webView.loadUrl("javascript:markAsRead" + "('" + it.toString() + "')") + if (view != null) binding.discussionRepliesWebViewWrapper.webView.loadUrl("javascript:markAsRead" + "('" + entryId.toString() + "')") } } + if (!groupDiscussion) { DiscussionTopicHeaderEvent(discussionTopicHeader).post() } @@ -263,7 +269,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { } private fun askToDeleteDiscussionEntry(discussionEntryId: Long) { - if (APIHelper.hasNetworkConnection()) { + if (repository.isOnline()) { val builder = AlertDialog.Builder(requireContext()) builder.setMessage(R.string.utils_discussionsDeleteWarning) builder.setPositiveButton(android.R.string.yes) { _, _ -> @@ -288,28 +294,32 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { } private fun deleteDiscussionEntry(entryId: Long) { - deleteDiscussionEntry(canvasContext, discussionTopicHeader.id, entryId, object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - if (response.code() in 200..299) { - discussionTopic?.let { - DiscussionUtils.findEntry(entryId, it.views)?.let { entry -> - entry.deleted = true - updateDiscussionAsDeleted(entry) - discussionTopicHeader.decrementDiscussionSubentryCount() - if (!groupDiscussion) { - DiscussionTopicHeaderEvent(discussionTopicHeader).post() - } + lifecycleScope.tryLaunch { + val result = repository.deleteDiscussionEntry(canvasContext, discussionTopicHeader.id, entryId) + + if (result is DataResult.Success) { + discussionTopic?.let { + DiscussionUtils.findEntry(entryId, it.views)?.let { entry -> + entry.deleted = true + updateDiscussionAsDeleted(entry) + discussionTopicHeader.decrementDiscussionSubentryCount() + if (!groupDiscussion) { + DiscussionTopicHeaderEvent(discussionTopicHeader).post() } } } } - }) + } } private fun showUpdateReplyView(discussionEntryId: Long) { - if (APIHelper.hasNetworkConnection()) { + if (repository.isOnline()) { discussionTopic?.let { - val route = DiscussionsUpdateFragment.makeRoute(canvasContext, discussionTopicHeader.id, DiscussionUtils.findEntry(discussionEntryId, it.views)) + val route = DiscussionsUpdateFragment.makeRoute( + canvasContext, + discussionTopicHeader.id, + DiscussionUtils.findEntry(discussionEntryId, it.views) + ) RouteMatcher.route(requireActivity(), route) } } else NoInternetConnectionDialog.show(requireFragmentManager()) @@ -320,34 +330,37 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { //region Liking private fun likeDiscussionPressed(discussionEntryId: Long) { - discussionTopic?.let { discussionTopic -> - if (discussionLikeJob?.isActive == true) return - - DiscussionUtils.findEntry(discussionEntryId, discussionTopic.views)?.let { entry -> - discussionLikeJob = tryWeave { + if (repository.isOnline()) { + discussionTopic?.let { discussionTopic -> + DiscussionUtils.findEntry(discussionEntryId, discussionTopic.views)?.let { entry -> val rating = if (discussionTopic.entryRatings.containsKey(discussionEntryId)) discussionTopic.entryRatings[discussionEntryId] else 0 val newRating = if (rating == 1) 0 else 1 - val response = awaitApiResponse { DiscussionManager.rateDiscussionEntry(canvasContext, discussionTopicHeader.id, discussionEntryId, newRating, it) } - if (response.code() in 200..299) { + lifecycleScope.tryLaunch { + repository.rateDiscussionEntry(canvasContext, discussionTopicHeader.id, discussionEntryId, newRating) + discussionTopic.entryRatings[discussionEntryId] = newRating if (newRating == 1) { entry.ratingSum += 1 entry._hasRated = true - updateDiscussionLiked(entry) + withContext(Dispatchers.Main) { + updateDiscussionLiked(entry) + } } else if (entry.ratingSum > 0) { entry.ratingSum -= 1 entry._hasRated = false - updateDiscussionUnliked(entry) + withContext(Dispatchers.Main) { + updateDiscussionUnliked(entry) + } } + } catch { + // Maybe a permissions issue? + Logger.e("Error liking discussion entry: " + it.message) } - } catch { - // Maybe a permissions issue? - Logger.e("Error liking discussion entry: " + it.message) } } - } + } else NoInternetConnectionDialog.show(requireFragmentManager()) } private fun updateDiscussionLiked(discussionEntry: DiscussionEntry) = updateDiscussionLikedState(discussionEntry, JS_CONST_SET_LIKED /*Constant found in the JS files*/) @@ -374,13 +387,12 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { webView.setBackgroundColor(requireContext().getColor(backgroundColorRes)) webView.settings.javaScriptEnabled = true webView.settings.useWideViewPort = true - webView.settings.allowFileAccess = true webView.settings.loadWithOverviewMode = true CookieManager.getInstance().acceptThirdPartyCookies(webView) webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun routeInternallyCallback(url: String) { if (!RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, routeIfPossible = true, allowUnsupported = false)) { - RouteMatcher.route(requireContext(), InternalWebviewFragment.makeRoute(url, url, false, "")) + RouteMatcher.route(requireActivity(), InternalWebviewFragment.makeRoute(url, url, false, "")) } } @@ -467,7 +479,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { @JavascriptInterface fun onMoreRepliesPressed(id: String) { discussionTopic?.let { - val route = DiscussionDetailsFragment.makeRoute(canvasContext, discussionTopicHeader, it, id.toLong()) + val route = makeRoute(canvasContext, discussionTopicHeader, it, id.toLong()) RouteMatcher.route(requireActivity(), route) } } @@ -521,20 +533,19 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { private fun getAuthenticatedURL(html: String, loadHtml: (newUrl: String, originalUrl: String?) -> Unit) { if (authenticatedSessionURL.isNullOrBlank()) { //get the url - sessionAuthJob = tryWeave { + lifecycleScope.tryLaunch { //get the url from html val matcher = Pattern.compile("src=\"([^\"]+)\"").matcher(discussionTopicHeader.message) matcher.find() val url = matcher.group(1) // Get an authenticated session so the user doesn't have to log in - authenticatedSessionURL = awaitApi { OAuthManager.getAuthenticatedSession(url, it) }.sessionUrl + authenticatedSessionURL = repository.getAuthenticatedSession(url)?.sessionUrl loadHtml(DiscussionUtils.getNewHTML(html, authenticatedSessionURL), url) } catch { //couldn't get the authenticated session, try to load it without it loadHtml(html, null) } - } else { loadHtml(DiscussionUtils.getNewHTML(html, authenticatedSessionURL), null) } @@ -544,7 +555,7 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { //region Loading private fun populateDiscussionData(forceRefresh: Boolean = false, topLevelReplyPosted: Boolean = false) = with(binding) { - discussionsLoadingJob = tryWeave { + lifecycleScope.tryLaunch { discussionProgressBar.setVisible() discussionRepliesWebViewWrapper.loadHtml("", "") discussionRepliesWebViewWrapper.setInvisible() @@ -560,18 +571,17 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { } if (courseId != null) { - courseSettings = CourseManager.getCourseSettingsAsync(courseId, forceRefresh).await().dataOrNull + courseSettings = repository.getCourseSettings(courseId, forceRefresh) } - if (forceRefresh) { val discussionTopicHeaderId = if (discussionTopicHeaderId == 0L && discussionTopicHeader.id != 0L) discussionTopicHeader.id else discussionTopicHeaderId if (!updateToGroupIfNecessary()) { - discussionTopicHeader = awaitApi { DiscussionManager.getDetailedDiscussion(canvasContext, discussionTopicHeaderId, it, true) } + discussionTopicHeader = repository.getDetailedDiscussion(canvasContext, discussionTopicHeaderId,true) } } else { // If there is no discussion (ID not set), then we need to load one if (discussionTopicHeader.id == 0L) { - discussionTopicHeader = awaitApi { DiscussionManager.getDetailedDiscussion(canvasContext, discussionTopicHeaderId, it, true) } + discussionTopicHeader = repository.getDetailedDiscussion(canvasContext, discussionTopicHeaderId, true) } } @@ -581,13 +591,12 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { loadDiscussionTopicHeaderViews(discussionTopicHeader) addAccessibilityButton() - // We only want to request the full discussion if it is not anonymous. Anonymous discussions are not supported by the API if (forceRefresh || discussionTopic == null && discussionTopicHeader.anonymousState == null) { // forceRefresh is true, fetch the discussion topic discussionTopic = getDiscussionTopic() - inBackground { discussionTopic?.views?.forEach { it.init(discussionTopic!!, it) } } + withContext(Dispatchers.IO){ discussionTopic?.views?.forEach { it.init(discussionTopic!!, it, repository.isOnline()) } } } if (discussionTopic == null || discussionTopic?.views?.isEmpty() == true && DiscussionCaching(discussionTopicHeader.id).isEmpty()) { @@ -600,15 +609,14 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { showAnonymousDiscussionView() } } else { - val html = inBackground { - DiscussionUtils.createDiscussionTopicHtml( + val html = DiscussionUtils.createDiscussionTopicHtml( requireContext(), isTablet, canvasContext, discussionTopicHeader, discussionTopic!!.views, - discussionEntryId) - } + discussionEntryId + ) loadDiscussionTopicViews(html) @@ -633,7 +641,10 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { swipeRefreshLayout.isEnabled = false openInBrowser.onClick { discussionTopicHeader.htmlUrl?.let { url -> - InternalWebviewFragment.loadInternalWebView(activity, InternalWebviewFragment.makeRoute(canvasContext, url, true, true)) + InternalWebviewFragment.loadInternalWebView( + activity, + InternalWebviewFragment.makeRoute(canvasContext, url, true, true) + ) } } } @@ -646,19 +657,41 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { changed = true canvasContext = groupPair.first discussionTopicHeaderId = groupPair.second - discussionTopicHeader = awaitApi { DiscussionManager.getDetailedDiscussion(canvasContext, discussionTopicHeaderId, it, true) } + discussionTopicHeader = repository.getDetailedDiscussion(canvasContext, discussionTopicHeaderId, true) } } return changed } + suspend fun getDiscussionGroup(discussionTopicHeader: DiscussionTopicHeader): Pair? { + var groups = emptyList() + ApiPrefs.user?.let { user -> + groups = repository.getAllGroups(user.id, true) + } + for (group in groups) { + val groupsMap = discussionTopicHeader.groupTopicChildren.associateBy({it.groupId}, {it.id}) + if (groupsMap.contains(group.id) && groupsMap[group.id] != null) { + groupsMap[group.id]?.let { topicHeaderId -> + return Pair(group, topicHeaderId) + } + + return null // There is a group, but not a matching topic header id + } + } + // If we made it to here, there are no groups that match this + return null + } + private suspend fun getDiscussionTopic(): DiscussionTopic { if (discussionTopicHeader.groupTopicChildren.isNotEmpty()) { // This is the base discussion for a group discussion // Grab the groups that the user belongs to - val userGroups = awaitApi> { GroupManager.getAllGroups(it, true) }.map { it.id } + var userGroups = emptyList() + ApiPrefs.user?.let { user -> + userGroups = repository.getAllGroups(user.id, true).map { it.id } + } // Match group from discussion to a group that the user is a part of var context = canvasContext @@ -672,10 +705,10 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { } } - return awaitApi { DiscussionManager.getFullDiscussionTopic(context, discussionId, true, it) } + return repository.getFullDiscussionTopic(context, discussionId, true) } else { // Regular discussion, fetch normally - return awaitApi { DiscussionManager.getFullDiscussionTopic(canvasContext, discussionTopicHeader.id, true, it) } + return repository.getFullDiscussionTopic(canvasContext, discussionTopicHeader.id, true) } } @@ -705,10 +738,10 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { discussionSection.setVisible(discussionTopicHeader.sections?.isNotEmpty() == true) replyToDiscussionTopic.setTextColor(ThemePrefs.textButtonColor) - replyToDiscussionTopic.setVisible(discussionTopicHeader.permissions!!.reply) + replyToDiscussionTopic.setVisible(discussionTopicHeader.permissions?.reply ?: false) replyToDiscussionTopic.onClick { showReplyView(discussionTopicHeader.id) } - loadHeaderHtmlJob = discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { + discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { if (view != null) loadHTMLTopic(it, discussionTopicHeader.title) }) @@ -729,13 +762,13 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { setupRepliesWebView() - loadRepliesHtmlJob = discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { + discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), html, "text/html", "UTF-8", null) }) swipeRefreshLayout.isRefreshing = false discussionTopicRepliesTitle.setVisible(discussionTopicHeader.shouldShowReplies) - postBeforeViewingRepliesTextView.setGone() + if (repository.isOnline()){ postBeforeViewingRepliesTextView.setGone() } } //endregion Loading @@ -800,7 +833,17 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { binding.alternateViewButton.apply { visibility = View.VISIBLE setOnClickListener { - RouteMatcher.route(requireActivity(), InternalWebviewFragment.makeRoute(canvasContext, discussionTopicHeader.htmlUrl!!, authenticate = true, shouldRouteInternally = false, allowRoutingTheSameUrlInternally = false, isUnsupportedFeature = false, allowUnsupportedRouting = false)) + RouteMatcher.route(requireActivity(), + InternalWebviewFragment.makeRoute( + canvasContext, + discussionTopicHeader.htmlUrl!!, + authenticate = true, + shouldRouteInternally = false, + allowRoutingTheSameUrlInternally = false, + isUnsupportedFeature = false, + allowUnsupportedRouting = false + ) + ) } } } @@ -911,22 +954,5 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { route.arguments.containsKey(DISCUSSION_TOPIC_HEADER_ID) || route.paramsHash.containsKey(RouterParams.MESSAGE_ID)) - suspend fun getDiscussionGroup(discussionTopicHeader: DiscussionTopicHeader): Pair? { - val groups = awaitApi> { - GroupManager.getAllGroups(it, false) - } - for (group in groups) { - val groupsMap = discussionTopicHeader.groupTopicChildren.associateBy({it.groupId}, {it.id}) - if (groupsMap.contains(group.id) && groupsMap[group.id] != null) { - groupsMap[group.id]?.let { topicHeaderId -> - return Pair(group, topicHeaderId) - } - - return null // There is a group, but not a matching topic header id - } - } - // If we made it to here, there are no groups that match this - return null - } } } diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepository.kt new file mode 100644 index 0000000000..cf9cfd47c5 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepository.kt @@ -0,0 +1,62 @@ +package com.instructure.student.features.discussion.details + +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsDataSource +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsLocalDataSource +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsNetworkDataSource + +class DiscussionDetailsRepository(localDataSource: DiscussionDetailsLocalDataSource, + private val networkDataSource: DiscussionDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun markAsRead(canvasContext: CanvasContext, discussionTopicHeaderId: Long, discussionEntryIds: List): List { + val successfullyMarkedAsReadIds: MutableList = mutableListOf() + discussionEntryIds.forEach { entryId -> + val result = networkDataSource.markAsRead(canvasContext, discussionTopicHeaderId, entryId) + if (result is DataResult.Success) { + successfullyMarkedAsReadIds.add(entryId) + } + } + return successfullyMarkedAsReadIds + } + + suspend fun deleteDiscussionEntry(canvasContext: CanvasContext, discussionTopicHeaderId: Long, entryId: Long): DataResult { + return networkDataSource.deleteDiscussionEntry(canvasContext, discussionTopicHeaderId, entryId) + } + + suspend fun rateDiscussionEntry(canvasContext: CanvasContext, discussionTopicHeaderId: Long, discussionEntryId: Long, rating: Int): DataResult { + return networkDataSource.rateDiscussionEntry(canvasContext, discussionTopicHeaderId, discussionEntryId, rating) + } + + suspend fun getAuthenticatedSession(url: String): AuthenticatedSession? { + return networkDataSource.getAuthenticatedSession(url).dataOrNull + } + + suspend fun getCourseSettings(courseId: Long, forceRefresh: Boolean): CourseSettings? { + return dataSource().getCourseSettings(courseId, forceRefresh).dataOrNull + } + + suspend fun getDetailedDiscussion(canvasContext: CanvasContext, discussionTopicHeaderId: Long, forceNetwork: Boolean): DiscussionTopicHeader { + return dataSource().getDetailedDiscussion(canvasContext, discussionTopicHeaderId, forceNetwork).dataOrThrow + } + + suspend fun getAllGroups(userId: Long, forceNetwork: Boolean): List { + return dataSource().getFirstPageGroups(userId, forceNetwork).depaginate { nextUrl -> dataSource().getNextPageGroups(nextUrl, forceNetwork) }.dataOrNull.orEmpty() + } + + suspend fun getFullDiscussionTopic(canvasContext: CanvasContext, topicId: Long, forceNetwork: Boolean): DiscussionTopic { + return dataSource().getFullDiscussionTopic(canvasContext, topicId, forceNetwork).dataOrThrow + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsDataSource.kt new file mode 100644 index 0000000000..0caadadd91 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsDataSource.kt @@ -0,0 +1,20 @@ +package com.instructure.student.features.discussion.details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult + +interface DiscussionDetailsDataSource { + suspend fun getCourseSettings(courseId: Long, forceRefresh: Boolean): DataResult + + suspend fun getDetailedDiscussion(canvasContext: CanvasContext, discussionTopicHeaderId: Long, forceNetwork: Boolean): DataResult + + suspend fun getFirstPageGroups(userId: Long, forceNetwork: Boolean): DataResult> + + suspend fun getNextPageGroups(nextUrl: String, forceNetwork: Boolean): DataResult> + + suspend fun getFullDiscussionTopic(canvasContext: CanvasContext, topicId: Long, forceNetwork: Boolean): DataResult +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSource.kt new file mode 100644 index 0000000000..da0d53677c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSource.kt @@ -0,0 +1,54 @@ +package com.instructure.student.features.discussion.details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.DiscussionTopicFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade + +class DiscussionDetailsLocalDataSource( + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + private val discussionTopicFacade: DiscussionTopicFacade, + private val courseSettingsDao: CourseSettingsDao, + private val groupFacade: GroupFacade, +) : DiscussionDetailsDataSource { + + override suspend fun getCourseSettings( + courseId: Long, + forceRefresh: Boolean + ): DataResult { + return DataResult.Success(courseSettingsDao.findByCourseId(courseId)?.toApiModel()) + } + + override suspend fun getDetailedDiscussion( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + forceNetwork: Boolean + ): DataResult { + return DataResult.Success(discussionTopicHeaderFacade.getDiscussionTopicHeaderById(discussionTopicHeaderId)) + } + + override suspend fun getFirstPageGroups(userId: Long, forceNetwork: Boolean): DataResult> { + return DataResult.Success(groupFacade.getGroupsByUserId(userId)) + } + + override suspend fun getNextPageGroups( + nextUrl: String, + forceNetwork: Boolean + ): DataResult> { + return DataResult.Success(emptyList()) + } + + override suspend fun getFullDiscussionTopic( + canvasContext: CanvasContext, + topicId: Long, + forceNetwork: Boolean + ): DataResult { + return DataResult.Success(discussionTopicFacade.getDiscussionTopic(topicId)) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..568671635e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSource.kt @@ -0,0 +1,89 @@ +package com.instructure.student.features.discussion.details.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult + +class DiscussionDetailsNetworkDataSource( + private val discussionApi: DiscussionAPI.DiscussionInterface, + private val oAuthApi: OAuthAPI.OAuthInterface, + private val courseApi: CourseAPI.CoursesInterface, + private val groupApi: GroupAPI.GroupInterface, +) : DiscussionDetailsDataSource { + suspend fun markAsRead( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + discussionEntryId: Long + ): DataResult { + val params = RestParams(isForceReadFromNetwork = true) + return discussionApi.markDiscussionTopicEntryRead(canvasContext.apiContext(), canvasContext.id, discussionTopicHeaderId, discussionEntryId, params) + } + + suspend fun deleteDiscussionEntry(canvasContext: CanvasContext, discussionTopicHeaderId: Long, entryId: Long): DataResult { + val params = RestParams(isForceReadFromNetwork = true) + return discussionApi.deleteDiscussionEntry(canvasContext.apiContext(), canvasContext.id, discussionTopicHeaderId, entryId, params) + } + + suspend fun rateDiscussionEntry( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + discussionEntryId: Long, + rating: Int + ): DataResult { + val params = RestParams(isForceReadFromNetwork = true) + return discussionApi.rateDiscussionEntry(canvasContext.apiContext(), canvasContext.id, discussionTopicHeaderId, discussionEntryId, rating, params) + } + + suspend fun getAuthenticatedSession(url: String): DataResult { + val params = RestParams(isForceReadFromNetwork = true) + return oAuthApi.getAuthenticatedSession(url, params) + } + + override suspend fun getCourseSettings( + courseId: Long, + forceRefresh: Boolean + ): DataResult { + val params = RestParams(isForceReadFromNetwork = forceRefresh) + return courseApi.getCourseSettings(courseId, params) + } + + override suspend fun getDetailedDiscussion( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + forceNetwork: Boolean + ): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return discussionApi.getDetailedDiscussion(canvasContext.apiContext(), canvasContext.id, discussionTopicHeaderId, params) + } + + override suspend fun getFirstPageGroups(userId: Long, forceNetwork: Boolean): DataResult> { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return groupApi.getFirstPageGroups(params) + } + + override suspend fun getNextPageGroups( + nextUrl: String, + forceNetwork: Boolean + ): DataResult> { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return groupApi.getNextPageGroups(nextUrl, params) + } + + override suspend fun getFullDiscussionTopic( + canvasContext: CanvasContext, + topicId: Long, + forceNetwork: Boolean + ): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return discussionApi.getFullDiscussionTopic(canvasContext.apiContext(), canvasContext.id, topicId, 1, params) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/DiscussionListFragment.kt similarity index 75% rename from apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/discussion/list/DiscussionListFragment.kt index 5a99ef3fff..e36dfe4a0a 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DiscussionListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/DiscussionListFragment.kt @@ -15,28 +15,23 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.discussion.list import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.Logger import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.interactions.bookmarks.Bookmarkable import com.instructure.interactions.bookmarks.Bookmarker import com.instructure.interactions.router.Route @@ -47,15 +42,17 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.DiscussionListRecyclerAdapter import com.instructure.student.databinding.CourseDiscussionTopicBinding import com.instructure.student.events.DiscussionCreatedEvent import com.instructure.student.events.DiscussionTopicHeaderDeletedEvent import com.instructure.student.events.DiscussionTopicHeaderEvent import com.instructure.student.events.DiscussionUpdatedEvent +import com.instructure.student.features.discussion.list.adapter.DiscussionListRecyclerAdapter +import com.instructure.student.fragment.CreateAnnouncementFragment +import com.instructure.student.fragment.CreateDiscussionFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -71,13 +68,15 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { @Inject lateinit var featureFlagProvider: FeatureFlagProvider + @Inject + lateinit var repository: DiscussionListRepository + protected var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: DiscussionListRecyclerAdapter + private var recyclerAdapter: DiscussionListRecyclerAdapter? = null private val linearLayoutManager by lazy { LinearLayoutManager(requireContext()) } private lateinit var discussionRecyclerView: RecyclerView - private var permissionJob: Job? = null private var canPost: Boolean = false private var groupsJob: WeaveJob? = null private var featureFlagsJob: WeaveJob? = null @@ -103,6 +102,8 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { requireContext(), canvasContext, !isAnnouncement, + repository, + lifecycleScope, object : DiscussionListRecyclerAdapter.AdapterToDiscussionsCallback { override fun onRowClicked(model: DiscussionTopicHeader, position: Int, isOpenDetail: Boolean) { RouteMatcher.route( @@ -115,7 +116,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { setRefreshing(false) // Show the FAB. if (canPost) createNewDiscussion.show() - if (recyclerAdapter.size() == 0) { + if (recyclerAdapter == null || recyclerAdapter?.size() == 0) { emptyView.let { if (isAnnouncement) { setEmptyView( @@ -141,36 +142,18 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { // Hide the FAB. if (canPost) binding.createNewDiscussion.hide() } - - override fun discussionOverflow(group: String?, discussionTopicHeader: DiscussionTopicHeader) { - if (group != null) { - // TODO - Blocked by COMMS-868 -// DiscussionsMoveToDialog.show(requireFragmentManager(), group, discussionTopicHeader, { newGroup -> -// recyclerAdapter.requestMoveDiscussionTopicToGroup(newGroup, group, discussionTopicHeader) -// }) - } - } - - override fun askToDeleteDiscussion(discussionTopicHeader: DiscussionTopicHeader) { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.utils_discussionsDeleteTitle) - .setMessage(R.string.utils_discussionsDeleteMessage) - .setPositiveButton(R.string.delete) { _, _ -> - recyclerAdapter.deleteDiscussionTopicHeader(discussionTopicHeader) - } - .setNegativeButton(R.string.cancel, null) - .showThemed() - } }) - this@DiscussionListFragment.discussionRecyclerView = configureRecyclerView( - binding.root, - requireContext(), - recyclerAdapter, - R.id.swipeRefreshLayout, - R.id.emptyView, - R.id.discussionRecyclerView - ) + recyclerAdapter?.let { adapter -> + this@DiscussionListFragment.discussionRecyclerView = configureRecyclerView( + binding.root, + requireContext(), + adapter, + R.id.swipeRefreshLayout, + R.id.emptyView, + R.id.discussionRecyclerView + ) + } linearLayoutManager.orientation = RecyclerView.VERTICAL discussionRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -205,7 +188,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { override fun onConfigurationChanged(newConfig: Configuration) = with(binding) { super.onConfigurationChanged(newConfig) - if (recyclerAdapter.size() == 0) { + if (recyclerAdapter == null || recyclerAdapter?.size() == 0) { emptyView.changeTextSize() if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (isTablet) { @@ -236,10 +219,9 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { override fun onDestroyView() { super.onDestroyView() - permissionJob?.cancel() featureFlagsJob?.cancel() groupsJob?.cancel() - recyclerAdapter.cancel() + recyclerAdapter?.cancel() } //endregion @@ -261,7 +243,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { } else { emptyView.emptyViewText(getString(R.string.noItemsMatchingQuery, query)) } - recyclerAdapter.searchQuery = query + recyclerAdapter?.searchQuery = query } ViewStyler.themeToolbarColored(requireActivity(), discussionListToolbar, canvasContext) } @@ -276,37 +258,14 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { //endregion private fun checkForPermission() { - permissionJob = tryWeave { - val permission = if (canvasContext.isCourse) { - awaitApi { - CourseManager.getCourse( - canvasContext.id, - it, - true - ) - }.permissions - } else { - awaitApi { - GroupManager.getDetailedGroup( - canvasContext.id, - it, - true - ) - }.permissions - } - - this@DiscussionListFragment.canvasContext.permissions = permission - canPost = if (isAnnouncement) { - permission?.canCreateAnnouncement ?: false - } else { - permission?.canCreateDiscussionTopic ?: false - } + lifecycleScope.tryLaunch { + canPost = repository.getCreationPermission(canvasContext, isAnnouncement) if (canPost) { - binding.createNewDiscussion.show() + if (view != null) binding.createNewDiscussion.show() } } catch { Logger.e("Error getting permissions for discussion permissions. " + it.message) - binding.createNewDiscussion.hide() + if (view != null) binding.createNewDiscussion.hide() } } @@ -315,7 +274,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onDiscussionUpdated(event: DiscussionUpdatedEvent) { event.once(javaClass.simpleName) { - recyclerAdapter.refresh() + recyclerAdapter?.refresh() } } @@ -335,13 +294,13 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { // Gets written over on phones - added also to {@link #onRefreshFinished()} when { it.pinned -> { - recyclerAdapter.addOrUpdateItem(DiscussionListRecyclerAdapter.PINNED, it) + recyclerAdapter?.addOrUpdateItem(DiscussionListRecyclerAdapter.PINNED, it) } it.locked -> { - recyclerAdapter.addOrUpdateItem(DiscussionListRecyclerAdapter.CLOSED_FOR_COMMENTS, it) + recyclerAdapter?.addOrUpdateItem(DiscussionListRecyclerAdapter.CLOSED_FOR_COMMENTS, it) } else -> { - recyclerAdapter.addOrUpdateItem(DiscussionListRecyclerAdapter.UNPINNED, it) + recyclerAdapter?.addOrUpdateItem(DiscussionListRecyclerAdapter.UNPINNED, it) } } } @@ -351,7 +310,7 @@ open class DiscussionListFragment : ParentFragment(), Bookmarkable { @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onDiscussionCreated(event: DiscussionCreatedEvent) { event.once(javaClass.simpleName) { - recyclerAdapter.refresh() + recyclerAdapter?.refresh() } } //endregion diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/DiscussionListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/DiscussionListRepository.kt new file mode 100644 index 0000000000..2d2a51b2b6 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/DiscussionListRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.pandautils.utils.isCourse +import com.instructure.student.features.discussion.list.datasource.DiscussionListDataSource +import com.instructure.student.features.discussion.list.datasource.DiscussionListLocalDataSource +import com.instructure.student.features.discussion.list.datasource.DiscussionListNetworkDataSource + +class DiscussionListRepository(localDataSource: DiscussionListLocalDataSource, + networkDataSource: DiscussionListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getCreationPermission(canvasContext: CanvasContext, isAnnouncements: Boolean): Boolean { + val permissions = if (canvasContext.isCourse) { + dataSource().getPermissionsForCourse(canvasContext as Course) + } else { + dataSource().getPermissionsForGroup(canvasContext as Group) + } + + return if (isAnnouncements) { + permissions?.canCreateAnnouncement ?: false + } else { + permissions?.canCreateDiscussionTopic ?: false + } + } + + suspend fun getDiscussionTopicHeaders(canvasContext: CanvasContext, isAnnouncements: Boolean, forceNetwork: Boolean): List { + return if (isAnnouncements) { + dataSource().getAnnouncements(canvasContext, forceNetwork) + } else { + dataSource().getDiscussions(canvasContext, forceNetwork) + } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/holders/DiscussionExpandableViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionExpandableViewHolder.kt similarity index 95% rename from apps/student/src/main/java/com/instructure/student/holders/DiscussionExpandableViewHolder.kt rename to apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionExpandableViewHolder.kt index c470b61698..1bb20dacf8 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/DiscussionExpandableViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionExpandableViewHolder.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.holders +package com.instructure.student.features.discussion.list.adapter import android.animation.AnimatorInflater import android.animation.ObjectAnimator @@ -22,7 +22,6 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import com.instructure.pandautils.utils.setVisible import com.instructure.student.R -import com.instructure.student.adapter.DiscussionListRecyclerAdapter import com.instructure.student.databinding.ViewholderDiscussionGroupHeaderBinding class DiscussionExpandableViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { diff --git a/apps/student/src/main/java/com/instructure/student/holders/DiscussionListHolder.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt similarity index 97% rename from apps/student/src/main/java/com/instructure/student/holders/DiscussionListHolder.kt rename to apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt index d4f1232aeb..e9673e4bdf 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/DiscussionListHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListHolder.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.holders +package com.instructure.student.features.discussion.list.adapter import android.content.Context import android.view.View @@ -28,9 +28,8 @@ import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setInvisible import com.instructure.pandautils.utils.setVisible import com.instructure.student.R -import com.instructure.student.adapter.DiscussionListRecyclerAdapter import com.instructure.student.databinding.ViewholderDiscussionBinding -import java.util.* +import java.util.Date class DiscussionListHolder(view: View) : RecyclerView.ViewHolder(view) { diff --git a/apps/student/src/main/java/com/instructure/student/adapter/DiscussionListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt similarity index 58% rename from apps/student/src/main/java/com/instructure/student/adapter/DiscussionListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt index efb4bef8e5..b98cb662c6 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/DiscussionListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt @@ -14,41 +14,35 @@ * along with this program. If not, see . * */ -package com.instructure.student.adapter +package com.instructure.student.features.discussion.list.adapter import android.content.Context -import androidx.recyclerview.widget.RecyclerView import android.view.View -import com.instructure.canvasapi2.StatusCallback -import com.instructure.canvasapi2.managers.AnnouncementManager -import com.instructure.canvasapi2.managers.DiscussionManager +import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.utils.ApiType -import com.instructure.canvasapi2.utils.LinkHeaders import com.instructure.canvasapi2.utils.filterWithQuery -import com.instructure.canvasapi2.utils.weave.awaitApi -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types -import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.textAndIconColor import com.instructure.pandautils.utils.toast import com.instructure.student.R -import com.instructure.student.holders.DiscussionExpandableViewHolder -import com.instructure.student.holders.DiscussionListHolder +import com.instructure.student.adapter.ExpandableRecyclerAdapter +import com.instructure.student.features.discussion.list.DiscussionListRepository import com.instructure.student.holders.EmptyViewHolder import com.instructure.student.holders.NoViewholder import com.instructure.student.interfaces.AdapterToFragmentCallback -import kotlinx.coroutines.Job -import retrofit2.Response -import java.util.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.Date open class DiscussionListRecyclerAdapter( context: Context, private val canvasContext: CanvasContext, private val isDiscussions: Boolean, + private val repository: DiscussionListRepository, + private val lifecycleScope: CoroutineScope, private val callback: AdapterToDiscussionsCallback, private val isTesting: Boolean = false ) : ExpandableRecyclerAdapter( @@ -59,8 +53,6 @@ open class DiscussionListRecyclerAdapter( private var discussions: List = emptyList() - private var discussionsListJob: Job? = null - var searchQuery = "" set(value) { field = value @@ -69,8 +61,6 @@ open class DiscussionListRecyclerAdapter( } interface AdapterToDiscussionsCallback : AdapterToFragmentCallback{ - fun discussionOverflow(group: String?, discussionTopicHeader: DiscussionTopicHeader) - fun askToDeleteDiscussion(discussionTopicHeader: DiscussionTopicHeader) fun onRefreshStarted() } @@ -113,15 +103,14 @@ open class DiscussionListRecyclerAdapter( override fun loadData() { callback.onRefreshStarted() - discussionsListJob = tryWeave { - discussions = awaitApi { - if (isDiscussions) DiscussionManager.getAllDiscussionTopicHeaders(canvasContext, isRefresh, it) - else AnnouncementManager.getAllAnnouncements(canvasContext, isRefresh, it) + lifecycleScope.launch { + try { + discussions = repository.getDiscussionTopicHeaders(canvasContext, !isDiscussions, isRefresh) + populateData() + } catch (e: Exception) { + callback.onRefreshFinished() + context.toast(R.string.errorOccurred) } - populateData() - } catch { - callback.onRefreshFinished() - context.toast(R.string.errorOccurred) } } @@ -139,10 +128,6 @@ open class DiscussionListRecyclerAdapter( adapterToRecyclerViewCallback.setIsEmpty(size() == 0) } - override fun cancel() { - discussionsListJob?.cancel() - } - private fun getHeaderType(discussionTopicHeader: DiscussionTopicHeader): String { if(discussionTopicHeader.pinned) return PINNED if(discussionTopicHeader.locked) return CLOSED_FOR_COMMENTS @@ -155,67 +140,6 @@ open class DiscussionListRecyclerAdapter( const val UNPINNED = "2_UNPINNED" const val CLOSED_FOR_COMMENTS = "3_CLOSED_FOR_COMMENTS" const val ANNOUNCEMENTS = "ANNOUNCEMENTS" - const val DELETE = "delete" - } - - private val mDiscussionTopicHeaderPinnedCallback = object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - response.body()?.let { addOrUpdateItem(PINNED, it) } - } - } - - private val mDiscussionTopicHeaderUnpinnedCallback = object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - response.body()?.let { addOrUpdateItem(UNPINNED, it) } - } - } - - private val mDiscussionTopicHeaderClosedForCommentsCallback = object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - response.body()?.let { addOrUpdateItem(CLOSED_FOR_COMMENTS, it) } - } - } - - private val mDiscussionTopicHeaderOpenedForCommentsCallback = object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - response.body()?.let { addOrUpdateItem(if(it.pinned) PINNED else UNPINNED, it) } - } - } - - fun requestMoveDiscussionTopicToGroup(groupTo: String, groupFrom: String, discussionTopicHeader: DiscussionTopicHeader) { - // Move from this group into another - when(groupFrom) { - PINNED -> { - when(groupTo) { - UNPINNED -> DiscussionManager.unpinDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, mDiscussionTopicHeaderUnpinnedCallback) - CLOSED_FOR_COMMENTS -> DiscussionManager.lockDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, mDiscussionTopicHeaderClosedForCommentsCallback) - DELETE -> { callback.askToDeleteDiscussion(discussionTopicHeader) } - } - } - UNPINNED -> { - when(groupTo) { - PINNED -> DiscussionManager.pinDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, mDiscussionTopicHeaderPinnedCallback) - CLOSED_FOR_COMMENTS -> DiscussionManager.lockDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, mDiscussionTopicHeaderClosedForCommentsCallback) - DELETE -> { callback.askToDeleteDiscussion(discussionTopicHeader) } - } - } - CLOSED_FOR_COMMENTS -> { - when(groupTo) { - PINNED -> DiscussionManager.pinDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, mDiscussionTopicHeaderPinnedCallback) - CLOSED_FOR_COMMENTS -> DiscussionManager.unlockDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, mDiscussionTopicHeaderOpenedForCommentsCallback) - DELETE -> { callback.askToDeleteDiscussion(discussionTopicHeader) } - } - } - DELETE -> { callback.askToDeleteDiscussion(discussionTopicHeader) } - } - } - - fun deleteDiscussionTopicHeader(discussionTopicHeader: DiscussionTopicHeader) { - DiscussionManager.deleteDiscussionTopicHeader(canvasContext, discussionTopicHeader.id, object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - removeItem(discussionTopicHeader, false) - } - }) } override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback { diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListDataSource.kt new file mode 100644 index 0000000000..78f4402cba --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListDataSource.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group + +interface DiscussionListDataSource { + + suspend fun getPermissionsForCourse(course: Course): CanvasContextPermission? + + suspend fun getPermissionsForGroup(group: Group): CanvasContextPermission? + + suspend fun getDiscussions(canvasContext: CanvasContext, forceNetwork: Boolean): List + + suspend fun getAnnouncements(canvasContext: CanvasContext, forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSource.kt new file mode 100644 index 0000000000..5bf39621f4 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSource.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade + +class DiscussionListLocalDataSource( + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade +) : DiscussionListDataSource { + + override suspend fun getPermissionsForCourse(course: Course): CanvasContextPermission? { + return null // Don't need to cache these because we can't create discussions/announcements offline. + } + + override suspend fun getPermissionsForGroup(group: Group): CanvasContextPermission? { + return null // Don't need to cache these because we can't create discussions/announcements offline. + } + + override suspend fun getDiscussions(canvasContext: CanvasContext, forceNetwork: Boolean): List { + return discussionTopicHeaderFacade.getDiscussionsForCourse(canvasContext.id) + } + + override suspend fun getAnnouncements(canvasContext: CanvasContext, forceNetwork: Boolean): List { + return discussionTopicHeaderFacade.getAnnouncementsForCourse(canvasContext.id) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSource.kt new file mode 100644 index 0000000000..8f1f87c60f --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSource.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list.datasource + +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.depaginate + +class DiscussionListNetworkDataSource( + private val courseApi: CourseAPI.CoursesInterface, + private val groupApi: GroupAPI.GroupInterface, + private val discussionApi: DiscussionAPI.DiscussionInterface, + private val announcementApi: AnnouncementAPI.AnnouncementInterface +) : DiscussionListDataSource { + override suspend fun getPermissionsForCourse(course: Course): CanvasContextPermission? { + val params = RestParams(isForceReadFromNetwork = true) + return courseApi.getCourse(course.id, params).dataOrNull?.permissions + } + + override suspend fun getPermissionsForGroup(group: Group): CanvasContextPermission? { + val params = RestParams(isForceReadFromNetwork = true) + return groupApi.getDetailedGroup(group.id, params).dataOrNull?.permissions + } + + override suspend fun getDiscussions(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return discussionApi.getFirstPageDiscussionTopicHeaders(canvasContext.apiContext(), canvasContext.id, params).depaginate { nextPage -> + discussionApi.getNextPage(nextPage, params) + }.dataOrThrow + } + + override suspend fun getAnnouncements(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return announcementApi.getFirstPageAnnouncementsList(canvasContext.apiContext(), canvasContext.id, params).depaginate { nextPage -> + announcementApi.getNextPageAnnouncementsList(nextPage, params) + }.dataOrThrow + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt new file mode 100644 index 0000000000..586dfa2ec2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt @@ -0,0 +1,38 @@ +package com.instructure.student.features.discussion.routing + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperLocalDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class DiscussionRouteHelperStudentRepository( + localDataSource: DiscussionRouteHelperLocalDataSource, + private val networkDataSource: DiscussionRouteHelperNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : DiscussionRouteHelperRepository, Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + override suspend fun getEnabledFeaturesForCourse( + canvasContext: CanvasContext, + forceNetwork: Boolean + ): Boolean { + return networkDataSource.getEnabledFeaturesForCourse(canvasContext, forceNetwork) + } + + override suspend fun getDiscussionTopicHeader( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + forceNetwork: Boolean + ): DiscussionTopicHeader? { + return dataSource().getDiscussionTopicHeader(canvasContext, discussionTopicHeaderId, forceNetwork) + } + + override suspend fun getAllGroups(discussionTopicHeader: DiscussionTopicHeader, userId: Long, forceNetwork: Boolean): List { + return dataSource().getAllGroups(discussionTopicHeader, userId, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/StudentDiscussionRouter.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt similarity index 72% rename from apps/student/src/main/java/com/instructure/student/features/discussion/StudentDiscussionRouter.kt rename to apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt index 47d98d8980..f2c309c9e6 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/StudentDiscussionRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/StudentDiscussionRouter.kt @@ -1,4 +1,4 @@ -package com.instructure.student.features.discussion +package com.instructure.student.features.discussion.routing import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext @@ -6,10 +6,15 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouter -import com.instructure.student.fragment.DiscussionDetailsFragment +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.details.DiscussionDetailsFragment import com.instructure.student.router.RouteMatcher -class StudentDiscussionRouter(private val fragmentActivity: FragmentActivity) : DiscussionRouter { +class StudentDiscussionRouter( + private val fragmentActivity: FragmentActivity, + private val networkStateProvider: NetworkStateProvider +) : DiscussionRouter { + override fun routeToDiscussion( canvasContext: CanvasContext, isRedesign: Boolean, @@ -17,7 +22,7 @@ class StudentDiscussionRouter(private val fragmentActivity: FragmentActivity) : isAnnouncement: Boolean ) { val route = when { - isRedesign -> DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeader) + isRedesign && networkStateProvider.isOnline() -> DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeader) else -> DiscussionDetailsFragment.makeRoute(canvasContext, discussionTopicHeader) } route.apply { diff --git a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseFragment.kt b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseFragment.kt index cdb0115c12..12ac118ccb 100644 --- a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCourseFragment.kt @@ -33,9 +33,9 @@ import com.instructure.pandautils.analytics.SCREEN_VIEW_ELEMENTARY_COURSE import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.* import com.instructure.student.databinding.FragmentElementaryCourseBinding -import com.instructure.student.fragment.CourseBrowserFragment -import com.instructure.student.fragment.GradesListFragment -import com.instructure.student.fragment.ModuleListFragment +import com.instructure.student.features.coursebrowser.CourseBrowserFragment +import com.instructure.student.features.grades.GradesListFragment +import com.instructure.student.features.modules.list.ModuleListFragment import com.instructure.student.router.RouteMatcher import dagger.hilt.android.AndroidEntryPoint @@ -122,7 +122,7 @@ class ElementaryCourseFragment : Fragment() { } private fun redirect(route: Route) { - RouteMatcher.route(requireContext(), route.copy(removePreviousScreen = true)) + RouteMatcher.route(requireActivity(), route.copy(removePreviousScreen = true)) } companion object { diff --git a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt index 03cc6d155b..f85bb77f0e 100644 --- a/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/elementary/course/ElementaryCoursePagerAdapter.kt @@ -75,28 +75,28 @@ class ElementaryCoursePagerAdapter( override fun onPageStartedCallback(webView: WebView, url: String) { progressBar.setVisible() } + override fun onPageFinishedCallback(webView: WebView, url: String) { progressBar.setGone() } override fun canRouteInternallyDelegate(url: String): Boolean { - return !isUrlSame(webView, url) && RouteMatcher.canRouteInternally(baseContext, url, ApiPrefs.domain, false) + return !isUrlSame(webView, url) && RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, false) } override fun routeInternallyCallback(url: String) { - RouteMatcher.canRouteInternally(baseContext, url, ApiPrefs.domain, true) + RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, true) } } - webView.canvasEmbeddedWebViewCallback = - object : CanvasWebView.CanvasEmbeddedWebViewCallback { - override fun shouldLaunchInternalWebViewFragment(url: String): Boolean { - return false - } - - override fun launchInternalWebViewFragment(url: String) { - activity?.startActivity(InternalWebViewActivity.createIntent(baseContext, url, "", true)) - } + webView.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { + override fun shouldLaunchInternalWebViewFragment(url: String): Boolean { + return false + } + + override fun launchInternalWebViewFragment(url: String) { + activity?.startActivity(InternalWebViewActivity.createIntent(baseContext, url, "", true)) } + } } private fun isUrlSame(webView: CanvasWebView, url: String): Boolean { diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsDataSource.kt new file mode 100644 index 0000000000..8d1dcf8c4b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsDataSource.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.details + +import com.instructure.canvasapi2.models.FileFolder + +interface FileDetailsDataSource { + suspend fun getFileFolderFromURL(url: String, fileId: Long, forceNetwork: Boolean): FileFolder? + +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FileDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt similarity index 79% rename from apps/student/src/main/java/com/instructure/student/fragment/FileDetailsFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt index 71ec7a8fca..6908031cde 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/FileDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.files.details import android.os.Bundle import android.text.Html @@ -23,11 +23,11 @@ import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import androidx.work.WorkManager import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions -import com.instructure.canvasapi2.managers.FileFolderManager -import com.instructure.canvasapi2.managers.ModuleManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.FileFolder import com.instructure.canvasapi2.models.ModuleObject @@ -48,9 +48,9 @@ import com.instructure.student.R import com.instructure.student.databinding.FragmentFileDetailsBinding import com.instructure.student.events.ModuleUpdatedEvent import com.instructure.student.events.post +import com.instructure.student.fragment.ParentFragment import com.instructure.student.util.StringUtilities import dagger.hilt.android.AndroidEntryPoint -import okhttp3.ResponseBody import java.util.* import javax.inject.Inject @@ -62,6 +62,9 @@ class FileDetailsFragment : ParentFragment() { @Inject lateinit var workManager: WorkManager + @Inject + lateinit var repository: FileDetailsRepository + private val binding by viewBinding(FragmentFileDetailsBinding::bind) private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -71,12 +74,7 @@ class FileDetailsFragment : ParentFragment() { private var file: FileFolder? = null private var fileUrl: String by StringArg(key = Const.FILE_URL) - - private var fileFolderJob: WeaveJob? = null - private var markAsReadJob: WeaveJob? = null - - private val fileId: Long - get() = file!!.id + private var fileId: Long by LongArg(key = Const.FILE_ID) private val moduleItemId: Long? get() = this.getModuleItemId() @@ -103,12 +101,6 @@ class FileDetailsFragment : ParentFragment() { getFileFolder() } - override fun onDestroyView() { - super.onDestroyView() - fileFolderJob?.cancel() - markAsReadJob?.cancel() - } - override fun applyTheme() { with (binding) { setupToolbarMenu(toolbar) @@ -126,8 +118,18 @@ class FileDetailsFragment : ParentFragment() { private fun setupClickListeners() { binding.openButton.setOnClickListener { - file?.let { - openMedia(it.contentType, it.url, it.displayName, canvasContext) + file?.let { fileFolder -> + when { + fileFolder.isLocalFile -> { + openLocalMedia( + fileFolder.contentType, + fileFolder.url, + fileFolder.displayName, + canvasContext + ) + } + else -> openMedia(fileFolder.contentType, fileFolder.url, fileFolder.displayName, canvasContext) + } markAsRead() } } @@ -139,6 +141,8 @@ class FileDetailsFragment : ParentFragment() { requestPermissions(PermissionUtils.makeArray(PermissionUtils.WRITE_EXTERNAL_STORAGE), PermissionUtils.WRITE_FILE_PERMISSION_REQUEST_CODE) } } + + binding.downloadButton.setVisible(repository.isOnline()) } override fun onMediaLoadingStarted() { @@ -156,21 +160,19 @@ class FileDetailsFragment : ParentFragment() { private fun markAsRead() { // Mark the module as read - markAsReadJob = tryWeave { - awaitApi { ModuleManager.markModuleItemAsRead(canvasContext, moduleObject.id, itemId, it) } + lifecycleScope.tryLaunch { + repository.markAsRead(canvasContext, moduleObject.id, itemId, true) ModuleUpdatedEvent(moduleObject).post() - } catch { + }.catch { Logger.e("Error marking module item as read. " + it.message) } } @Suppress("deprecation") private fun getFileFolder() = with(binding) { - fileFolderJob = tryWeave { - val response = awaitApiResponse { FileFolderManager.getFileFolderFromURL(fileUrl, true, it) } + lifecycleScope.tryLaunch { + file = repository.getFileFolderFromURL(fileUrl, fileId, true) // Set up everything else now, we should have a file - file = response.body() - file?.let { if (it.lockInfo != null) { // File is locked @@ -204,15 +206,28 @@ class FileDetailsFragment : ParentFragment() { setupTextViews() setupClickListeners() // If the file has a thumbnail then show it. Make it a little bigger since the thumbnail size is pretty small - if (!TextUtils.isEmpty(it.thumbnailUrl)) { - - fileIcon.layoutParams.apply { - height = requireActivity().DP(230).toInt() - width = height + if (repository.isOnline()) { + if (!TextUtils.isEmpty(it.thumbnailUrl)) { + + fileIcon.layoutParams.apply { + height = requireActivity().DP(230).toInt() + width = height + } + + fileIcon.contentDescription = + getString(R.string.filePreviewContentDescription) + Glide.with(requireActivity()).load(it.thumbnailUrl) + .apply(RequestOptions().fitCenter()).into(fileIcon) + } + } + else { + if (it.contentType?.contains("image") == true) { + fileIcon.layoutParams.apply { + height = requireActivity().DP(230).toInt() + width = height + } + fileIcon.setImageURI(it.url?.toUri()) } - - fileIcon.contentDescription = getString(R.string.filePreviewContentDescription) - Glide.with(requireActivity()).load(it.thumbnailUrl).apply(RequestOptions().fitCenter()).into(fileIcon) } } setPageViewReady() @@ -234,16 +249,20 @@ class FileDetailsFragment : ParentFragment() { companion object { - fun makeRoute(canvasContext: CanvasContext, fileUrl: String): Route { - val bundle = Bundle().apply { putString(Const.FILE_URL, fileUrl) } + fun makeRoute(canvasContext: CanvasContext, fileUrl: String, fileId: Long): Route { + val bundle = Bundle().apply { + putString(Const.FILE_URL, fileUrl) + putLong(Const.FILE_ID, fileId) + } return Route(null, FileDetailsFragment::class.java, canvasContext, bundle) } - fun makeRoute(canvasContext: CanvasContext, moduleObject: ModuleObject, itemId: Long, fileUrl: String): Route { + fun makeRoute(canvasContext: CanvasContext, moduleObject: ModuleObject, itemId: Long, fileUrl: String, fileId: Long): Route { val bundle = Bundle().apply { putString(Const.FILE_URL, fileUrl) putParcelable(Const.MODULE_OBJECT, moduleObject) putLong(Const.ITEM_ID, itemId) + putLong(Const.FILE_ID, fileId) } return Route(null, FileDetailsFragment::class.java, canvasContext, bundle) } diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsLocalDataSource.kt new file mode 100644 index 0000000000..d30fa16aad --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsLocalDataSource.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.details + +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao + +class FileDetailsLocalDataSource( + private val fileFolderDao: FileFolderDao, + private val localFileFolderDao: LocalFileDao, +) : FileDetailsDataSource { + override suspend fun getFileFolderFromURL(url: String, fileId: Long, forceNetwork: Boolean): FileFolder? { + val file = fileFolderDao.findById(fileId) ?: return null + val localFile = localFileFolderDao.findById(fileId) ?: return null + return file.copy(url = localFile.path).toApiModel() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..668abe8725 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsNetworkDataSource.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.details + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import okhttp3.ResponseBody + +class FileDetailsNetworkDataSource( + private val moduleApi: ModuleAPI.ModuleInterface, + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface, +) : FileDetailsDataSource { + suspend fun markAsRead(canvasContext: CanvasContext, moduleId: Long, itemId: Long, forceNetwork: Boolean): ResponseBody? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return moduleApi.markModuleItemRead(canvasContext.apiContext(), canvasContext.id, moduleId, itemId, restParams).dataOrNull + } + + override suspend fun getFileFolderFromURL(url: String, fileId: Long, forceNetwork: Boolean): FileFolder? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return fileFolderApi.getFileFolderFromURL(url, restParams).dataOrNull + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsRepository.kt new file mode 100644 index 0000000000..361ce825e9 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/details/FileDetailsRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import okhttp3.ResponseBody + +class FileDetailsRepository( + localDataSource: FileDetailsLocalDataSource, + private val networkDataSource: FileDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun markAsRead(canvasContext: CanvasContext, moduleId: Long, itemId: Long, forceNetwork: Boolean): ResponseBody? { + return networkDataSource.markAsRead(canvasContext, moduleId, itemId, forceNetwork) + } + + suspend fun getFileFolderFromURL(url: String, fileId: Long, forceNetwork: Boolean): FileFolder? { + return dataSource().getFileFolderFromURL(url, fileId, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListDataSource.kt new file mode 100644 index 0000000000..e70ce6dcac --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult + +interface FileListDataSource { + + suspend fun getFolders(folderId: Long, forceNetwork: Boolean): DataResult> + + suspend fun getFiles(folderId: Long, forceNetwork: Boolean): DataResult> + + suspend fun getFolder(folderId: Long, forceNetwork: Boolean): FileFolder? + + suspend fun getRootFolderForContext(canvasContext: CanvasContext, forceNetwork: Boolean): FileFolder? + + suspend fun getNextPage(url: String, forceNetwork: Boolean): DataResult> +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt similarity index 82% rename from apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt index d945d5913e..ace8cd4d9f 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListFragment.kt @@ -1,20 +1,21 @@ /* - * Copyright (C) 2016 - present Instructure, Inc. + * Copyright (C) 2023 - present Instructure, Inc. * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . * */ -package com.instructure.student.fragment +package com.instructure.student.features.files.list import android.content.DialogInterface import android.content.res.Configuration @@ -28,6 +29,7 @@ import android.view.animation.AnimationUtils import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu +import androidx.core.content.FileProvider import androidx.fragment.app.DialogFragment import androidx.lifecycle.LiveData import androidx.work.WorkInfo @@ -53,20 +55,22 @@ import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment import com.instructure.pandautils.features.file.upload.FileUploadDialogParent import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.FileFolderCallback -import com.instructure.student.adapter.FileListRecyclerAdapter import com.instructure.student.databinding.FragmentFileListBinding import com.instructure.student.dialog.EditTextDialog import com.instructure.student.features.files.search.FileSearchFragment +import com.instructure.student.fragment.InternalWebviewFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.StudentPrefs import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import java.io.File import java.util.* import javax.inject.Inject + @ScreenView(SCREEN_VIEW_FILE_LIST) @PageView @AndroidEntryPoint @@ -75,6 +79,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent @Inject lateinit var workManager: WorkManager + @Inject + lateinit var fileListRepository: FileListRepository + private val binding by viewBinding(FragmentFileListBinding::bind) private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -135,7 +142,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.search) { - RouteMatcher.route(requireContext(), Route(FileSearchFragment::class.java, canvasContext, Bundle())) + RouteMatcher.route(requireActivity(), Route(FileSearchFragment::class.java, canvasContext, Bundle())) return true } return super.onOptionsItemSelected(item) @@ -155,10 +162,10 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent tryWeave { folder = if (folderId != 0L) { // If folderId is valid, get folder by ID - awaitApi { FileFolderManager.getFolder(folderId, true, it) } + fileListRepository.getFolder(folderId, true) } else { // Otherwise get root folder of the CanvasContext - awaitApi { FileFolderManager.getRootFolderForContext(canvasContext, true, it) } + fileListRepository.getRootFolderForContext(canvasContext, true) } configureViews() } catch { @@ -203,23 +210,24 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent adapterCallback = object : FileFolderCallback { override fun onItemClicked(item: FileFolder) { - if (item.fullName != null) { - RouteMatcher.route(requireContext(), FileListFragment.makeRoute(canvasContext, item)) - } else { - recordFilePreviewEvent(item) - if (item.isHtmlFile) { - /* An HTML file can reference other canvas files as resources (e.g. CSS files) and must be - accessed as an authenticated preview to work correctly */ - RouteMatcher.route(requireContext(), InternalWebviewFragment.makeRoute( - canvasContext = canvasContext, - url = item.getFilePreviewUrl(ApiPrefs.fullDomain, canvasContext), - authenticate = true, - isUnsupportedFeature = false, - allowUnsupportedRouting = false, - shouldRouteInternally = true, - allowRoutingTheSameUrlInternally = false - )) - } else { + when { + item.fullName != null -> { + RouteMatcher.route(requireActivity(), makeRoute(canvasContext, item)) + } + item.isHtmlFile && item.isLocalFile -> { + recordFilePreviewEvent(item) + openHtmlFile(item) + } + item.isHtmlFile && !item.isLocalFile -> { + recordFilePreviewEvent(item) + openHtmlUrl(item) + } + item.isLocalFile -> { + recordFilePreviewEvent(item) + openLocalMedia(item.contentType, item.url, item.displayName, canvasContext) + } + else -> { + recordFilePreviewEvent(item) openMedia(item.contentType, item.url, item.displayName, canvasContext) } } @@ -265,6 +273,40 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent } } + private fun openHtmlUrl(fileFolder: FileFolder) { + /* An HTML file can reference other canvas files as resources (e.g. CSS files) and must be + accessed as an authenticated preview to work correctly */ + RouteMatcher.route( + requireActivity(), InternalWebviewFragment.makeRoute( + canvasContext = canvasContext, + url = fileFolder.getFilePreviewUrl(ApiPrefs.fullDomain, canvasContext), + authenticate = true, + isUnsupportedFeature = false, + allowUnsupportedRouting = false, + shouldRouteInternally = true, + allowRoutingTheSameUrlInternally = false + ) + ) + } + + private fun openHtmlFile(fileFolder: FileFolder) { + fileFolder.url?.let { + val file = File(it) + val uri = FileProvider.getUriForFile(requireContext(), requireContext().applicationContext.packageName + Const.FILE_PROVIDER_AUTHORITY, file) + RouteMatcher.route( + requireActivity(), InternalWebviewFragment.makeRoute( + canvasContext = canvasContext, + url = uri.toString(), + authenticate = false, + isUnsupportedFeature = false, + allowUnsupportedRouting = false, + shouldRouteInternally = true, + allowRoutingTheSameUrlInternally = false + ) + ) + } + } + private fun themeToolbar() = with(binding) { // We style the toolbar white for user files if (canvasContext.type == CanvasContext.Type.USER) { @@ -280,7 +322,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent val isUserFiles = canvasContext.type == CanvasContext.Type.USER if (recyclerAdapter == null) { - recyclerAdapter = FileListRecyclerAdapter(requireContext(), canvasContext, getFileMenuOptions(folder!!, canvasContext), folder!!, adapterCallback) + recyclerAdapter = FileListRecyclerAdapter(requireContext(), canvasContext, getFileMenuOptions(folder, canvasContext, fileListRepository.isOnline()), folder, adapterCallback, fileListRepository) } configureRecyclerView(requireView(), requireContext(), recyclerAdapter!!, R.id.swipeRefreshLayout, R.id.emptyView, R.id.listView) @@ -288,7 +330,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent setupToolbarMenu(toolbar) // Update toolbar title with folder name if it's not a root folder - if (!folder!!.isRoot) toolbar.title = folder?.name + if (folder?.isRoot == false) toolbar.title = folder?.name themeToolbar() @@ -324,7 +366,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent val popup = PopupMenu(requireContext(), anchorView) popup.inflate(R.menu.file_folder_options) with(popup.menu) { - val options = getFileMenuOptions(item, canvasContext) + val options = getFileMenuOptions(item, canvasContext, fileListRepository.isOnline()) // Only show alternate-open option for PDF files findItem(R.id.openAlternate).isVisible = options.contains(FileMenuType.OPEN_IN_ALTERNATE) findItem(R.id.download).isVisible = options.contains(FileMenuType.DOWNLOAD) @@ -336,7 +378,11 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent when (menuItem.itemId) { R.id.openAlternate -> { recordFilePreviewEvent(item) - openMedia(item.contentType, item.url, item.displayName, true, canvasContext) + if (fileListRepository.isOnline()) { + openMedia(item.contentType, item.url, item.displayName, true, canvasContext) + } else { + openLocalMedia(item.contentType, item.url, item.displayName, canvasContext, true) + } } R.id.download -> downloadItem(item) R.id.rename -> renameItem(item) @@ -379,7 +425,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent awaitApi { FileFolderManager.updateFolder(item.id, body, it) } } recyclerAdapter?.add(updateItem) - StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds + folder!!.id + if (folder != null) { + StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds + folder!!.id + } } catch { toast(R.string.errorOccurred) } @@ -422,7 +470,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent if (recyclerAdapter?.size() == 0) { setEmptyView(binding.emptyView, R.drawable.ic_panda_nofiles, R.string.noFiles, getNoFileSubtextId()) } - StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds + folder!!.id + if (folder != null) { + StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds + folder!!.id + } updateFileList() } catch { toast(R.string.errorOccurred) @@ -468,6 +518,7 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent private fun createFolder() { EditTextDialog.show(requireFragmentManager(), getString(R.string.createFolder), "") { name -> tryWeave { + if (folder == null) throw IllegalArgumentException("Folder is null") val newFolder = awaitApi { FileFolderManager.createFolder(folder!!.id, CreateFolder(name), it) } @@ -566,7 +617,8 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent /** * @return A list of possible actions the user is able to perform on the file/folder */ - fun getFileMenuOptions(fileFolder: FileFolder, canvasContext: CanvasContext): List { + fun getFileMenuOptions(fileFolder: FileFolder?, canvasContext: CanvasContext, isOnline: Boolean): List { + if (fileFolder == null) return emptyList() val options: MutableList = mutableListOf() if (canvasContext.type == CanvasContext.Type.USER) { @@ -600,7 +652,9 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent // User is a student; Students can only download course files if (!fileFolder.isLockedForUser && fileFolder.isFile) { // File isn't locked, let them download it - options.add(FileMenuType.DOWNLOAD) + if (isOnline) { + options.add(FileMenuType.DOWNLOAD) + } if ("pdf" in fileFolder.contentType.orEmpty()) { options.add(FileMenuType.OPEN_IN_ALTERNATE) @@ -615,7 +669,10 @@ class FileListFragment : ParentFragment(), Bookmarkable, FileUploadDialogParent } if (fileFolder.isFile) { - options.add(FileMenuType.DOWNLOAD) + if (isOnline) { + options.add(FileMenuType.DOWNLOAD) + } + if ("pdf" in fileFolder.contentType.orEmpty()) { options.add(FileMenuType.OPEN_IN_ALTERNATE) } diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListLocalDataSource.kt new file mode 100644 index 0000000000..26261881b6 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListLocalDataSource.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao + +class FileListLocalDataSource( + private val fileFolderDao: FileFolderDao, + private val localFileDao: LocalFileDao +) : FileListDataSource { + override suspend fun getFolders(folderId: Long, forceNetwork: Boolean): DataResult> { + return DataResult.Success(fileFolderDao.findVisibleFoldersByParentId(folderId).map { it.toApiModel() }) + } + + override suspend fun getFiles(folderId: Long, forceNetwork: Boolean): DataResult> { + val files = fileFolderDao.findVisibleFilesByFolderId(folderId).map { it.toApiModel() } + val fileIds = files.map { it.id } + val localFileMap = localFileDao.findByIds(fileIds).associate { it.id to it.path } + + return DataResult.Success(files.map { + it.copy(url = localFileMap[it.id], thumbnailUrl = null) + }) + } + + override suspend fun getFolder(folderId: Long, forceNetwork: Boolean): FileFolder? { + return fileFolderDao.findById(folderId)?.toApiModel() + } + + override suspend fun getRootFolderForContext(canvasContext: CanvasContext, forceNetwork: Boolean): FileFolder? { + return fileFolderDao.findRootFolderForContext(canvasContext.id)?.toApiModel() + } + + override suspend fun getNextPage(url: String, forceNetwork: Boolean): DataResult> = DataResult.Success(emptyList()) +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListNetworkDataSource.kt new file mode 100644 index 0000000000..e71b808fe8 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListNetworkDataSource.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.list + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class FileListNetworkDataSource(private val fileFolderApi: FileFolderAPI.FilesFoldersInterface) : FileListDataSource { + override suspend fun getFolders(folderId: Long, forceNetwork: Boolean): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return fileFolderApi.getFirstPageFolders(folderId, restParams) + } + + override suspend fun getFiles(folderId: Long, forceNetwork: Boolean): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return fileFolderApi.getFirstPageFiles(folderId, restParams) + } + + override suspend fun getFolder(folderId: Long, forceNetwork: Boolean): FileFolder? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return fileFolderApi.getFolder(folderId, restParams).dataOrNull + } + + override suspend fun getRootFolderForContext(canvasContext: CanvasContext, forceNetwork: Boolean): FileFolder? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return fileFolderApi.getRootFolderForContext(canvasContext.id, canvasContext.type.apiString, restParams).dataOrNull + } + + override suspend fun getNextPage(url: String, forceNetwork: Boolean): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return fileFolderApi.getNextPageFileFoldersList(url, restParams) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/adapter/FileListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt similarity index 58% rename from apps/student/src/main/java/com/instructure/student/adapter/FileListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt index 8897e148d8..35f5dae3fd 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/FileListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRecyclerAdapter.kt @@ -1,41 +1,42 @@ /* - * Copyright (C) 2016 - present Instructure, Inc. + * Copyright (C) 2023 - present Instructure, Inc. * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . * */ -package com.instructure.student.adapter +package com.instructure.student.features.files.list import android.content.Context import android.view.View -import com.instructure.canvasapi2.managers.FileFolderManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitPaginated import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.pandautils.utils.textAndIconColor -import com.instructure.student.fragment.FileListFragment +import com.instructure.student.adapter.BaseListRecyclerAdapter import com.instructure.student.holders.FileViewHolder import com.instructure.student.util.StudentPrefs open class FileListRecyclerAdapter( - context: Context, - val canvasContext: CanvasContext, - private val possibleMenuOptions: List, // Used for testing, see protected constructor below - private val folder: FileFolder, - private val fileFolderCallback: FileFolderCallback + context: Context, + val canvasContext: CanvasContext, + private val possibleMenuOptions: List, // Used for testing, see protected constructor below + private val folder: FileFolder?, + private val fileFolderCallback: FileFolderCallback, + private val fileListRepository: FileListRepository ) : BaseListRecyclerAdapter(context, FileFolder::class.java) { private var isTesting = false @@ -50,13 +51,14 @@ open class FileListRecyclerAdapter( possibleMenuOptions: List, folder: FileFolder, itemCallback: FileFolderCallback, - isTesting: Boolean - ) : this(context, canvasContext, possibleMenuOptions, folder, itemCallback) { + isTesting: Boolean, + fileListRepository: FileListRepository + ) : this(context, canvasContext, possibleMenuOptions, folder, itemCallback, fileListRepository) { this.isTesting = isTesting } init { - itemCallback = object : BaseListRecyclerAdapter.ItemComparableCallback() { + itemCallback = object : ItemComparableCallback() { override fun compare(o1: FileFolder, o2: FileFolder) = o1.compareTo(o2) override fun areContentsTheSame(item1: FileFolder, item2: FileFolder) = compareFileFolders(item1, item2) override fun areItemsTheSame(item1: FileFolder, item2: FileFolder) = item1.id == item2.id @@ -66,7 +68,7 @@ open class FileListRecyclerAdapter( } override fun bindHolder(item: FileFolder, holder: FileViewHolder, position: Int) { - holder.bind(item, contextColor, context, FileListFragment.getFileMenuOptions(item, canvasContext ), fileFolderCallback) + holder.bind(item, contextColor, context, FileListFragment.getFileMenuOptions(item, canvasContext, fileListRepository.isOnline()), fileFolderCallback) } override fun createViewHolder(v: View, viewType: Int) = FileViewHolder(v) @@ -78,41 +80,25 @@ open class FileListRecyclerAdapter( override fun loadFirstPage() { apiCall?.cancel() apiCall = tryWeave { - + if (folder == null) { + setNextUrl(null) + throw IllegalArgumentException("Folder is null") + } // Check if the folder is marked as stale (i.e. items were added/changed/removed) val isStale = StudentPrefs.staleFolderIds.contains(folder.id) // Force network for pull-to-refresh and stale folders val forceNetwork = isRefresh || isStale - // Get folders - awaitPaginated> { - onRequestFirst { FileFolderManager.getFirstPageFolders(folder.id, forceNetwork, it) } - onRequestNext { url, callback -> FileFolderManager.getNextPageFilesFolder(url, forceNetwork, callback) } - onResponse { - setNextUrl("") - addAll(it) - } - } - - // Get files - awaitPaginated> { - onRequestFirst { FileFolderManager.getFirstPageFiles(folder.id, forceNetwork, it) } - onRequestNext { url, callback -> FileFolderManager.getNextPageFilesFolder(url, forceNetwork, callback) } - onResponse { - setNextUrl("") - // Files don't tell us if they're for submissions; set it on each file here - // based on the folder's state to make it easier for us later on when we need - // to determine how the user can interact with the file - addAll(it.apply { it.forEach { it.forSubmissions = folder.forSubmissions }}) - } + val items = fileListRepository.getFirstPageItems(folder.id, forceNetwork) + addAll(items.dataOrThrow) + if (items is DataResult.Success) { + setNextUrl(items.linkHeaders.nextUrl) } // Mark folder as no longer stale if (isStale) StudentPrefs.staleFolderIds = StudentPrefs.staleFolderIds - folder.id - isAllPagesLoaded = true - setNextUrl(null) fileFolderCallback.onRefreshFinished() onCallbackFinished() } catch { @@ -122,7 +108,20 @@ open class FileListRecyclerAdapter( } override fun loadNextPage(nextURL: String) { - apiCall?.next() + apiCall = tryWeave { + if (folder == null) { + setNextUrl(null) + throw IllegalArgumentException("Folder is null") + } + val nextResult = fileListRepository.getNextPage(nextURL, folder.id, isRefresh) + addAll(nextResult.dataOrThrow) + if (nextResult is DataResult.Success) { + setNextUrl(nextResult.linkHeaders.nextUrl) + } + } catch { + fileFolderCallback.onRefreshFinished() + onCallbackFinished() + } } private fun compareFileFolders(oldItem: FileFolder, newItem: FileFolder): Boolean { diff --git a/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRepository.kt new file mode 100644 index 0000000000..acd9003770 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/files/list/FileListRepository.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.files.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class FileListRepository( + fileListLocalDataSource: FileListLocalDataSource, + fileListNetworkDataSource: FileListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository( + fileListLocalDataSource, + fileListNetworkDataSource, + networkStateProvider, + featureFlagProvider +) { + + suspend fun getFirstPageItems(folderId: Long, forceNetwork: Boolean): DataResult> { + val foldersResult = getFirstPageFolders(folderId, forceNetwork) + return when { + foldersResult.isSuccess && (foldersResult as DataResult.Success).linkHeaders.nextUrl == null -> { + val filesResult = getFirstPageFiles(folderId, forceNetwork) + if (filesResult is DataResult.Success) { + DataResult.Success(foldersResult.data + filesResult.data, filesResult.linkHeaders) + } else { + filesResult + } + } + + else -> foldersResult + } + } + + private suspend fun getFirstPageFolders(folderId: Long, forceNetwork: Boolean): DataResult> { + return dataSource().getFolders(folderId, forceNetwork) + } + + suspend fun getNextPage(nextUrl: String, folderId: Long, forceNetwork: Boolean): DataResult> { + val nextResult = getNextPage(nextUrl, forceNetwork) + return when { + nextResult.isSuccess && (nextResult as DataResult.Success).linkHeaders.nextUrl == null && !nextUrl.contains("files") -> { + val filesResult = getFirstPageFiles(folderId, forceNetwork) + if (filesResult is DataResult.Success) { + DataResult.Success(nextResult.data + filesResult.data, filesResult.linkHeaders) + } else { + filesResult + } + } + else -> nextResult + } + } + + private suspend fun getNextPage(url: String, forceNetwork: Boolean): DataResult> { + return dataSource().getNextPage(url, forceNetwork) + } + + private suspend fun getFirstPageFiles(folderId: Long, forceNetwork: Boolean): DataResult> { + return dataSource().getFiles(folderId, forceNetwork) + } + + suspend fun getFolder(folderId: Long, forceNetwork: Boolean): FileFolder? { + return dataSource().getFolder(folderId, forceNetwork) + } + + suspend fun getRootFolderForContext(canvasContext: CanvasContext, forceNetwork: Boolean): FileFolder? { + return dataSource().getRootFolderForContext(canvasContext, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt index 08b9431f56..d0b4a54768 100644 --- a/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/files/search/FileSearchAdapter.kt @@ -28,7 +28,7 @@ import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.pandautils.utils.textAndIconColor import com.instructure.student.adapter.BaseListRecyclerAdapter -import com.instructure.student.adapter.FileFolderCallback +import com.instructure.student.features.files.list.FileFolderCallback import com.instructure.student.holders.FileViewHolder class FileSearchAdapter( diff --git a/apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt similarity index 81% rename from apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt index bf7008a265..d5b10ebda6 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.grades import android.content.res.Configuration import android.os.Bundle @@ -26,7 +26,6 @@ import android.widget.AdapterView import android.widget.Toast import androidx.core.content.ContextCompat import com.google.android.material.appbar.AppBarLayout -import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView @@ -41,21 +40,26 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.GradesListRecyclerAdapter import com.instructure.student.adapter.TermSpinnerAdapter import com.instructure.student.databinding.FragmentCourseGradesBinding import com.instructure.student.dialog.WhatIfDialogStyled -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.router.RouteMatcher -import retrofit2.Response +import dagger.hilt.android.AndroidEntryPoint import java.math.BigDecimal import java.math.RoundingMode +import javax.inject.Inject @ScreenView(SCREEN_VIEW_GRADES_LIST) @PageView(url = "{canvasContext}/grades") +@AndroidEntryPoint class GradesListFragment : ParentFragment(), Bookmarkable { + @Inject + lateinit var repository: GradesListRepository + private val binding by viewBinding(FragmentCourseGradesBinding::bind) private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -88,26 +92,34 @@ class GradesListFragment : ParentFragment(), Bookmarkable { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_course_grades, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - recyclerAdapter = GradesListRecyclerAdapter(requireContext(), course, adapterToFragmentCallback, adapterToGradesCallback, gradingPeriodsCallback, object : WhatIfDialogStyled.WhatIfDialogCallback { - override fun onClick(assignment: Assignment, position: Int) { - WhatIfDialogStyled.show(requireFragmentManager(), assignment, course.textAndIconColor) { whatIf, _ -> - //Create dummy submission for what if grade - //check to see if grade is empty for reset - if (whatIf == null) { - assignment.submission = null - recyclerAdapter.assignmentsHash[assignment.id]?.submission = null - } else { - recyclerAdapter.assignmentsHash[assignment.id]?.submission = Submission( + recyclerAdapter = GradesListRecyclerAdapter( + requireContext(), + course, + adapterToFragmentCallback, + repository, + ::onGradingPeriodResponse, + adapterToGradesCallback, + object : WhatIfDialogStyled.WhatIfDialogCallback { + override fun onClick(assignment: Assignment, position: Int) { + WhatIfDialogStyled.show(requireFragmentManager(), assignment, course.textAndIconColor) { whatIf, _ -> + //Create dummy submission for what if grade + //check to see if grade is empty for reset + if (whatIf == null) { + assignment.submission = null + recyclerAdapter.assignmentsHash[assignment.id]?.submission = null + } else { + recyclerAdapter.assignmentsHash[assignment.id]?.submission = Submission( score = whatIf, grade = whatIf.toString() - ) - } + ) + } - //Compute new overall grade - computeGrades(binding.showTotalCheckBox.isChecked, position) + //Compute new overall grade + computeGrades(binding.showTotalCheckBox.isChecked, position) + } } } - }) + ) view.let { configureViews(it) configureRecyclerView(it, requireContext(), recyclerAdapter, R.id.swipeRefreshLayout, R.id.gradesEmptyView, R.id.listView) @@ -233,7 +245,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { private val adapterToFragmentCallback = object : AdapterToFragmentCallback { override fun onRowClicked(assignment: Assignment, position: Int, isOpenDetail: Boolean) { - RouteMatcher.route(requireContext(), AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) + RouteMatcher.route(requireActivity(), AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) } override fun onRefreshFinished() { @@ -241,63 +253,56 @@ class GradesListFragment : ParentFragment(), Bookmarkable { } } - /* - * This code is similar to code in the AssignmentListFragment. - * If you make changes here, make sure to check the same callback in the AssignmentListFrag. - */ - private val gradingPeriodsCallback = object : StatusCallback() { - - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - if (view == null) return - with(binding) { - gradingPeriodsList = ArrayList() - gradingPeriodsList.addAll(response.body()!!.gradingPeriodList) - // Add "select all" option - gradingPeriodsList.add(allTermsGradingPeriod) - termAdapter = TermSpinnerAdapter( - requireContext(), - android.R.layout.simple_spinner_dropdown_item, - gradingPeriodsList - ) - termSpinner.adapter = termAdapter - termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) {} - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - // The current item must always be set first - recyclerAdapter.currentGradingPeriod = termAdapter?.getItem(position) - if (termAdapter?.getItem(position)?.title == getString(R.string.allGradingPeriods)) { - recyclerAdapter.loadData() - } else { - if (termAdapter?.isEmpty == false) { - recyclerAdapter.loadAssignmentsForGradingPeriod( - termAdapter?.getItem(position)?.id ?: 0, - true, - true - ) - termSpinner.isEnabled = false - termAdapter?.isLoading = true - termAdapter?.notifyDataSetChanged() - } + private fun onGradingPeriodResponse(gradingPeriodList: List) { + if (view == null) return + with(binding) { + gradingPeriodsList = ArrayList() + gradingPeriodsList.addAll(gradingPeriodList) + // Add "select all" option + gradingPeriodsList.add(allTermsGradingPeriod) + termAdapter = TermSpinnerAdapter( + requireContext(), + android.R.layout.simple_spinner_dropdown_item, + gradingPeriodsList + ) + termSpinner.adapter = termAdapter + termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) {} + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + // The current item must always be set first + recyclerAdapter.currentGradingPeriod = termAdapter?.getItem(position) + if (termAdapter?.getItem(position)?.title == getString(R.string.allGradingPeriods)) { + recyclerAdapter.loadData() + } else { + if (termAdapter?.isEmpty == false) { + recyclerAdapter.loadAssignmentsForGradingPeriod( + gradingPeriodID = termAdapter?.getItem(position)?.id.orDefault(), + refreshFirst = true, + forceNetwork = true + ) + termSpinner.isEnabled = false + termAdapter?.isLoading = true + termAdapter?.notifyDataSetChanged() } - showTotalCheckBox.isChecked = true } + showTotalCheckBox.isChecked = true } + } - // If we have a "current" grading period select it - if (recyclerAdapter.currentGradingPeriod != null) { - val position = termAdapter?.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: -1) ?: -1 - if (position != -1) { - termSpinner.setSelection(position) - } else { - Toast.makeText( - requireActivity(), - com.instructure.loginapi.login.R.string.errorOccurred, - Toast.LENGTH_SHORT - ).show() - } + // If we have a "current" grading period select it + if (recyclerAdapter.currentGradingPeriod != null) { + val position = termAdapter?.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: -1) ?: -1 + if (position != -1) { + termSpinner.setSelection(position) + } else { + Toast.makeText( + requireActivity(), + com.instructure.loginapi.login.R.string.errorOccurred, + Toast.LENGTH_SHORT + ).show() } - termSpinner.setVisible() } + termSpinner.setVisible() } } diff --git a/apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt similarity index 86% rename from apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt index 9dca6b4615..bc534a568d 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRecyclerAdapter.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.adapter +package com.instructure.student.features.grades import android.content.Context import android.view.View @@ -38,26 +38,25 @@ import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.isNullOrEmpty -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types -import com.instructure.pandautils.utils.ColorKeeper import com.instructure.student.R +import com.instructure.student.adapter.ExpandableRecyclerAdapter import com.instructure.student.dialog.WhatIfDialogStyled import com.instructure.student.holders.EmptyViewHolder import com.instructure.student.holders.ExpandableViewHolder import com.instructure.student.holders.GradeViewHolder import com.instructure.student.interfaces.AdapterToFragmentCallback import kotlinx.coroutines.* -import java.util.ArrayList -import java.util.HashMap +@OptIn(DelicateCoroutinesApi::class) open class GradesListRecyclerAdapter( context: Context, var canvasContext: CanvasContext? = null, val adapterToFragmentCallback: AdapterToFragmentCallback? = null, + private val repository: GradesListRepository, + private val onGradingPeriodResponse: (List) -> Unit, private val adapterToGradesCallback: AdapterToGradesCallback? = null, - private val gradingPeriodsCallback: StatusCallback? = null, private val whatIfDialogCallback: WhatIfDialogStyled.WhatIfDialogCallback? = null ) : ExpandableRecyclerAdapter(context, AssignmentGroup::class.java, Assignment::class.java) { @@ -128,7 +127,7 @@ open class GradesListRecyclerAdapter( try { // Logic regarding MGP is similar here as it is in both assignment recycler adapters, // if changes are made here, check if they are needed in the other recycler adapters - val course = awaitApi{CourseManager.getCourseWithGrade(canvasContext!!.id, it, forceNetwork)} + val course = repository.getCourseWithGrade(canvasContext!!.id, forceNetwork) val enrollments = (canvasContext as Course).enrollments canvasContext = course @@ -180,19 +179,20 @@ open class GradesListRecyclerAdapter( // We need to use an ID from an observee, not the user (who is currently logged in as an observer) when retrieving the enrollments // Get the first student this user is observing, if none show empty assignments - val student = awaitApi> { EnrollmentManager.getObserveeEnrollments(forceNetwork, it) } + val student = repository.getObserveeEnrollments(forceNetwork) .firstOrNull { it.courseId == course.id && it.observedUser != null }?.observedUser - ?: return@launch updateAssignmentGroups(emptyList()) + ?: return@launch updateAssignmentGroups(emptyList()) // Get Assignment Groups - val assignmentGroups = awaitApi> { AssignmentManager.getAssignmentGroupsWithAssignmentsForGradingPeriod(canvasContext!!.id, currentGradingPeriod!!.id, - scopeToStudent = false, - forceNetwork = forceNetwork, - callback = it - ) } + val assignmentGroups = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod( + canvasContext!!.id, + currentGradingPeriod!!.id, + false, + forceNetwork + ) // The assignments in the assignment groups do not come with their submissions (with the associated grades), so we get them all here val assignmentIds = assignmentGroups.map { it.assignments }.flatten().map { it.id } - val submissions = awaitApi>{ SubmissionManager.getSubmissionsForMultipleAssignments(student.id, course.id, assignmentIds, it, forceNetwork) } + val submissions = repository.getSubmissionsForMultipleAssignments(student.id, course.id, assignmentIds, forceNetwork) assignmentGroups.forEach { group -> group.assignments.forEach { assignment -> assignment.submission = submissions.firstOrNull { it.assignmentId == assignment.id } @@ -201,25 +201,23 @@ open class GradesListRecyclerAdapter( updateAssignmentGroups(assignmentGroups) - awaitApi> { CourseManager.getCoursesWithSyllabus(forceNetwork, it) } - .onEach { course -> - course.enrollments?.find { it.userId == student.id }?.let { - course.enrollments = mutableListOf(it) - courseGrade = course.getCourseGradeFromEnrollment(it, false) - val restrictQuantitativeData = course.settings?.restrictQuantitativeData ?: false - adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData, course.gradingScheme) - } + repository.getCoursesWithSyllabus(forceNetwork).onEach { course -> + course.enrollments?.find { it.userId == student.id }?.let { + course.enrollments = mutableListOf(it) + courseGrade = course.getCourseGradeFromEnrollment(it, false) + val restrictQuantitativeData = course.settings?.restrictQuantitativeData ?: false + adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData, course.gradingScheme) } + } } catch (e: CancellationException) { //We cancelled the job, nothing to do here } catch (e: Throwable) { Toast.makeText(context, R.string.errorOccurred, Toast.LENGTH_SHORT).show() } } - return } - private fun setupStudentGrades(enrollment: Enrollment, course: Course, forceNetwork: Boolean) { + private suspend fun setupStudentGrades(enrollment: Enrollment, course: Course, forceNetwork: Boolean) { if (currentGradingPeriod == null || currentGradingPeriod?.title == null) { // We load current term currentGradingPeriod = GradingPeriod( @@ -228,13 +226,11 @@ open class GradesListRecyclerAdapter( ) // Request the grading period objects and make the assignment calls - // This callback is fulfilled in the grade list fragment. - CourseManager.getGradingPeriodsForCourse(gradingPeriodsCallback!!, course.id, forceNetwork) - return + val result = repository.getGradingPeriodsForCourse(course.id, forceNetwork) + if (result.isNotEmpty()) onGradingPeriodResponse(result) } else { // Otherwise we load the info from the current grading period loadAssignmentsForGradingPeriod(currentGradingPeriod!!.id, true, forceNetwork) - return } } @@ -250,12 +246,22 @@ open class GradesListRecyclerAdapter( if (scopeToStudent) { assignmentsForGradingPeriodJob = GlobalScope.launch(Dispatchers.Main) { try { - val assignmentGroups = awaitApi>{AssignmentManager.getAssignmentGroupsWithAssignmentsForGradingPeriod(canvasContext!!.id, gradingPeriodID, scopeToStudent, forceNetwork, it)} + val assignmentGroups = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod( + canvasContext!!.id, + gradingPeriodID, + true, + forceNetwork + ) updateAssignmentGroups(assignmentGroups) // Fetch the enrollments associated with the selected gradingPeriodID, these will contain the // correct grade for the period - val enrollments = awaitApi>{CourseManager.getUserEnrollmentsForGradingPeriod(canvasContext!!.id, ApiPrefs.user!!.id, gradingPeriodID, it, forceNetwork)} + val enrollments = repository.getUserEnrollmentsForGradingPeriod( + canvasContext!!.id, + ApiPrefs.user!!.id, + gradingPeriodID, + forceNetwork + ) updateCourseGradeFromGradingPeriodSpecificEnrollment(enrollments) // Inform the spinner things are done @@ -295,7 +301,7 @@ open class GradesListRecyclerAdapter( allAssignmentsAndGroupsJob = GlobalScope.launch(Dispatchers.Main) { try { // Standard load assignments, unfiltered - val aGroups = awaitApi>{AssignmentManager.getAssignmentGroupsWithAssignments(canvasContext!!.id, forceNetwork, it)} + val aGroups = repository.getAssignmentGroupsWithAssignments(canvasContext!!.id, forceNetwork) updateAssignmentGroups(aGroups) } catch (e: CancellationException) { //We cancelled the job, nothing to do here @@ -321,7 +327,7 @@ open class GradesListRecyclerAdapter( for (assignment in gradedAssignments) { assignmentsHash[assignment.id] = assignment } - if (!assignmentGroups.contains(group)) { + if (assignmentGroups.none { it.id == group.id }) { assignmentGroups.add(group) } } @@ -420,7 +426,6 @@ open class GradesListRecyclerAdapter( override fun cancel() { super.cancel() - gradingPeriodsCallback?.cancel() observerCourseGradeJob?.cancel() loadDataJob?.cancel() allAssignmentsAndGroupsJob?.cancel() diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt new file mode 100644 index 0000000000..75d6c9088a --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/grades/GradesListRepository.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades + +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.grades.datasource.GradesListDataSource +import com.instructure.student.features.grades.datasource.GradesListLocalDataSource +import com.instructure.student.features.grades.datasource.GradesListNetworkDataSource + +class GradesListRepository( + localDataSource: GradesListLocalDataSource, + networkDataSource: GradesListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course { + return dataSource().getCourseWithGrade(courseId, forceNetwork) + } + + suspend fun getObserveeEnrollments(forceNetwork: Boolean): List { + return dataSource().getObserveeEnrollments(forceNetwork) + } + + suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List { + return dataSource().getAssignmentGroupsWithAssignmentsForGradingPeriod(courseId, gradingPeriodId, scopeToStudent, forceNetwork) + } + + suspend fun getSubmissionsForMultipleAssignments( + studentId: Long, + courseId: Long, + assignmentIds: List, + forceNetwork: Boolean + ): List { + return dataSource().getSubmissionsForMultipleAssignments(studentId, courseId, assignmentIds, forceNetwork) + } + + suspend fun getCoursesWithSyllabus(forceNetwork: Boolean): List { + return dataSource().getCoursesWithSyllabus(forceNetwork) + } + + suspend fun getGradingPeriodsForCourse(courseId: Long, forceNetwork: Boolean): List { + return dataSource().getGradingPeriodsForCourse(courseId, forceNetwork) + } + + suspend fun getUserEnrollmentsForGradingPeriod( + courseId: Long, + userId: Long, + gradingPeriodId: Long, + forceNetwork: Boolean + ): List { + return dataSource().getUserEnrollmentsForGradingPeriod(courseId, userId, gradingPeriodId, forceNetwork) + } + + suspend fun getAssignmentGroupsWithAssignments( + courseId: Long, + forceNetwork: Boolean, + ): List { + return dataSource().getAssignmentGroupsWithAssignments(courseId, forceNetwork) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListDataSource.kt new file mode 100644 index 0000000000..8dc725da1a --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListDataSource.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades.datasource + +import com.instructure.canvasapi2.models.* + +interface GradesListDataSource { + + suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course + + suspend fun getObserveeEnrollments(forceNetwork: Boolean): List + + suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List + + suspend fun getSubmissionsForMultipleAssignments( + studentId: Long, + courseId: Long, + assignmentIds: List, + forceNetwork: Boolean + ): List + + suspend fun getCoursesWithSyllabus(forceNetwork: Boolean): List + + suspend fun getGradingPeriodsForCourse(courseId: Long, forceNetwork: Boolean): List + + suspend fun getUserEnrollmentsForGradingPeriod( + courseId: Long, + userId: Long, + gradingPeriodId: Long, + forceNetwork: Boolean + ): List + + suspend fun getAssignmentGroupsWithAssignments( + courseId: Long, + forceNetwork: Boolean, + ): List +} diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSource.kt new file mode 100644 index 0000000000..092861db05 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSource.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades.datasource + +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade + +class GradesListLocalDataSource( + private val courseFacade: CourseFacade, + private val enrollmentFacade: EnrollmentFacade, + private val assignmentFacade: AssignmentFacade, + private val submissionFacade: SubmissionFacade +) : GradesListDataSource { + + override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course { + return courseFacade.getCourseById(courseId) ?: throw IllegalStateException("Could not load from DB") + } + + override suspend fun getObserveeEnrollments(forceNetwork: Boolean): List { + return enrollmentFacade.getAllEnrollments() + } + + override suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List { + return assignmentFacade.getAssignmentGroupsWithAssignmentsForGradingPeriod(courseId, gradingPeriodId) + } + + override suspend fun getSubmissionsForMultipleAssignments( + studentId: Long, + courseId: Long, + assignmentIds: List, + forceNetwork: Boolean + ): List { + return submissionFacade.findByAssignmentIds(assignmentIds) + } + + override suspend fun getCoursesWithSyllabus(forceNetwork: Boolean): List { + return courseFacade.getAllCourses() + } + + override suspend fun getGradingPeriodsForCourse(courseId: Long, forceNetwork: Boolean): List { + return courseFacade.getGradingPeriodsByCourseId(courseId) + } + + override suspend fun getUserEnrollmentsForGradingPeriod( + courseId: Long, + userId: Long, + gradingPeriodId: Long, + forceNetwork: Boolean + ): List { + return enrollmentFacade.getEnrollmentsByGradingPeriodId(gradingPeriodId) + } + + override suspend fun getAssignmentGroupsWithAssignments(courseId: Long, forceNetwork: Boolean): List { + return assignmentFacade.getAssignmentGroupsWithAssignments(courseId) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSource.kt new file mode 100644 index 0000000000..db3487c4c2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSource.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades.datasource + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.depaginate + +class GradesListNetworkDataSource( + private val courseApi: CourseAPI.CoursesInterface, + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, + private val assignmentApi: AssignmentAPI.AssignmentInterface, + private val submissionApi: SubmissionAPI.SubmissionInterface, +) : GradesListDataSource { + + override suspend fun getCourseWithGrade(courseId: Long, forceNetwork: Boolean): Course { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return courseApi.getCourseWithGrade(courseId, params).dataOrThrow + } + + override suspend fun getObserveeEnrollments(forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return enrollmentApi.firstPageObserveeEnrollments(params).depaginate { + enrollmentApi.getNextPage(it, params) + }.dataOrThrow + } + + override suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long, + scopeToStudent: Boolean, + forceNetwork: Boolean + ): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return assignmentApi.getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + courseId = courseId, + gradingPeriodId = gradingPeriodId, + scopeToStudent = scopeToStudent, + restParams = params + ).depaginate { + assignmentApi.getNextPageAssignmentGroupListWithAssignmentsForGradingPeriod(it, params) + }.dataOrThrow + } + + override suspend fun getSubmissionsForMultipleAssignments( + studentId: Long, + courseId: Long, + assignmentIds: List, + forceNetwork: Boolean + ): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return submissionApi.getSubmissionsForMultipleAssignments( + courseId, studentId, assignmentIds, params + ).depaginate { + submissionApi.getNextPageSubmissions(it, params) + }.dataOrThrow + } + + override suspend fun getCoursesWithSyllabus(forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return courseApi.firstPageCoursesWithSyllabus(params).depaginate { + courseApi.next(it, params) + }.dataOrThrow + } + + override suspend fun getGradingPeriodsForCourse(courseId: Long, forceNetwork: Boolean): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return courseApi.getGradingPeriodsForCourse(courseId, params).dataOrThrow.gradingPeriodList + } + + override suspend fun getUserEnrollmentsForGradingPeriod( + courseId: Long, + userId: Long, + gradingPeriodId: Long, + forceNetwork: Boolean + ): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return courseApi.getUserEnrollmentsForGradingPeriod(courseId, userId, gradingPeriodId, params).dataOrThrow + } + + override suspend fun getAssignmentGroupsWithAssignments(courseId: Long, forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return assignmentApi.getFirstPageAssignmentGroupListWithAssignments(courseId, params).depaginate { + assignmentApi.getNextPageAssignmentGroupListWithAssignments(it, params) + }.dataOrThrow + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt b/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt index afa67a4e9d..ef195aaa84 100644 --- a/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/login/StudentAcceptableUsePolicyRouter.kt @@ -22,13 +22,17 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.services.PushNotificationRegistrationWorker import com.instructure.student.R import com.instructure.student.activity.InternalWebViewActivity import com.instructure.student.activity.NavigationActivity import com.instructure.student.tasks.StudentLogoutTask -class StudentAcceptableUsePolicyRouter(private val activity: FragmentActivity) : AcceptableUsePolicyRouter { +class StudentAcceptableUsePolicyRouter( + private val activity: FragmentActivity, + private val databaseProvider: DatabaseProvider +) : AcceptableUsePolicyRouter { override fun openPolicy(content: String) { val intent = InternalWebViewActivity.createIntent(activity, "http://www.canvaslms.com/policies/terms-of-use", content, activity.getString(R.string.acceptableUsePolicyTitle), false) @@ -50,6 +54,6 @@ class StudentAcceptableUsePolicyRouter(private val activity: FragmentActivity) : } override fun logout() { - StudentLogoutTask(LogoutTask.Type.LOGOUT).execute() + StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute() } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt b/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt index 25bdd70535..8c562f1760 100644 --- a/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt +++ b/apps/student/src/main/java/com/instructure/student/features/login/StudentLoginNavigation.kt @@ -22,15 +22,19 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.loginapi.login.LoginNavigation import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.services.PushNotificationRegistrationWorker import com.instructure.student.activity.NavigationActivity import com.instructure.student.tasks.StudentLogoutTask -class StudentLoginNavigation(private val activity: FragmentActivity) : LoginNavigation(activity) { +class StudentLoginNavigation( + private val activity: FragmentActivity, + private val databaseProvider: DatabaseProvider +) : LoginNavigation(activity) { override val checkElementary: Boolean = true override fun logout() { - StudentLogoutTask(LogoutTask.Type.LOGOUT).execute() + StudentLogoutTask(LogoutTask.Type.LOGOUT, databaseProvider = databaseProvider).execute() } override fun initMainActivityIntent(): Intent { diff --git a/apps/student/src/main/java/com/instructure/student/util/CollapsedModulesStore.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/CollapsedModulesStore.kt similarity index 94% rename from apps/student/src/main/java/com/instructure/student/util/CollapsedModulesStore.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/list/CollapsedModulesStore.kt index 3134bf662f..01e5c5ca84 100644 --- a/apps/student/src/main/java/com/instructure/student/util/CollapsedModulesStore.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/CollapsedModulesStore.kt @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.instructure.student.util +package com.instructure.student.features.modules.list import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.student.util.StudentPrefs object CollapsedModulesStore { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt similarity index 87% rename from apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt index ef31cd8b4d..134ea38aea 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListFragment.kt @@ -15,13 +15,14 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.modules.list import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ModuleItem @@ -37,21 +38,26 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.ModuleListRecyclerAdapter import com.instructure.student.databinding.FragmentModuleListBinding import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding import com.instructure.student.events.ModuleUpdatedEvent -import com.instructure.student.interfaces.ModuleAdapterToFragmentCallback +import com.instructure.student.features.modules.list.adapter.ModuleAdapterToFragmentCallback +import com.instructure.student.features.modules.list.adapter.ModuleListRecyclerAdapter +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment +import com.instructure.student.features.modules.util.ModuleProgressionUtility +import com.instructure.student.features.modules.util.ModuleUtility +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.CourseModulesStore -import com.instructure.student.util.ModuleProgressionUtility -import com.instructure.student.util.ModuleUtility +import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import javax.inject.Inject @ScreenView(SCREEN_VIEW_MODULE_LIST) @PageView(url = "modules") +@AndroidEntryPoint class ModuleListFragment : ParentFragment(), Bookmarkable { private val binding by viewBinding(FragmentModuleListBinding::bind) @@ -60,6 +66,9 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { private lateinit var recyclerAdapter: ModuleListRecyclerAdapter + @Inject + lateinit var repository: ModuleListRepository + val tabId: String get() = Tab.MODULES_ID @@ -75,11 +84,6 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { EventBus.getDefault().unregister(this) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - retainInstance = true - } - override fun onDestroy() { recyclerAdapter.cancel() super.onDestroy() @@ -144,7 +148,8 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { fun setupViews() { val navigatingToSpecificModule = !arguments?.getString(MODULE_ID).isNullOrEmpty() - recyclerAdapter = ModuleListRecyclerAdapter(canvasContext, requireContext(), navigatingToSpecificModule, object : ModuleAdapterToFragmentCallback { + recyclerAdapter = ModuleListRecyclerAdapter(canvasContext, requireContext(), navigatingToSpecificModule, repository, lifecycleScope, object : + ModuleAdapterToFragmentCallback { override fun onRowClicked(moduleObject: ModuleObject, moduleItem: ModuleItem, position: Int, isOpenDetail: Boolean) { if (moduleItem.type != null && moduleItem.type == ModuleObject.State.UnlockRequirements.apiString) return @@ -157,15 +162,20 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { // Remove all the subheaders and stuff. val groups = recyclerAdapter.groups - val moduleItemsArray = groups.indices.mapTo(ArrayList>()) { recyclerAdapter.getItems(groups[it]) } - val moduleHelper = ModuleProgressionUtility.prepareModulesForCourseProgression(requireContext(), moduleItem.id, groups, moduleItemsArray) + val moduleItemsArray = groups.indices.mapTo(ArrayList()) { recyclerAdapter.getItems(groups[it]) } + val moduleHelper = ModuleProgressionUtility.prepareModulesForCourseProgression( + requireContext(), moduleItem.id, groups, moduleItemsArray + ) CourseModulesStore.moduleListItems = moduleHelper.strippedModuleItems CourseModulesStore.moduleObjects = groups - RouteMatcher.route(requireContext(), CourseModuleProgressionFragment.makeRoute( + RouteMatcher.route( + requireActivity(), CourseModuleProgressionFragment.makeRoute( canvasContext, moduleHelper.newGroupPosition, - moduleHelper.newChildPosition)) + moduleHelper.newChildPosition + ) + ) } override fun onRefreshFinished(isError: Boolean) { @@ -181,7 +191,9 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { // that appear near the end of the list will not have the extra 'expanded' space needed // to scroll as far as possible toward the top recyclerBinding.listView.postDelayed({ - val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString(MODULE_ID)!!.toLong()) + val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString( + MODULE_ID + )!!.toLong()) if (groupPosition >= 0) { val lm = recyclerBinding.listView.layoutManager as? LinearLayoutManager lm?.scrollToPositionWithOffset(groupPosition, 0) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt new file mode 100644 index 0000000000..4dbfd3dc89 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/ModuleListRepository.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.modules.list.datasource.ModuleListDataSource +import com.instructure.student.features.modules.list.datasource.ModuleListLocalDataSource +import com.instructure.student.features.modules.list.datasource.ModuleListNetworkDataSource + +class ModuleListRepository( + localDataSource: ModuleListLocalDataSource, + private val networkDataSource: ModuleListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getAllModuleObjects( + canvasContext: CanvasContext, + forceNetwork: Boolean + ): DataResult> { + return dataSource().getAllModuleObjects(canvasContext, forceNetwork) + } + + suspend fun getFirstPageModuleObjects( + canvasContext: CanvasContext, + forceNetwork: Boolean + ): DataResult> { + return dataSource().getFirstPageModuleObjects(canvasContext, forceNetwork) + } + + suspend fun getNextPageModuleObjects(nextUrl: String, forceNetwork: Boolean): DataResult> { + return networkDataSource.getNextPageModuleObjects(nextUrl, forceNetwork) + } + + suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val tabs = dataSource().getTabs(canvasContext, forceNetwork).dataOrNull ?: emptyList() + return tabs.filter { !(it.isExternal && it.isHidden) } + } + + suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return dataSource().loadCourseSettings(courseId, forceNetwork) + } + + suspend fun getFirstPageModuleItems( + canvasContext: CanvasContext, + moduleId: Long, + forceNetwork: Boolean + ): DataResult> { + return dataSource().getFirstPageModuleItems(canvasContext, moduleId, forceNetwork) + } + + suspend fun getNextPageModuleItems(nextUrl: String, forceNetwork: Boolean): DataResult> { + return networkDataSource.getNextPageModuleItems(nextUrl, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/interfaces/ModuleAdapterToFragmentCallback.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleAdapterToFragmentCallback.kt similarity index 94% rename from apps/student/src/main/java/com/instructure/student/interfaces/ModuleAdapterToFragmentCallback.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleAdapterToFragmentCallback.kt index 7f47ca11ae..aa9fadff94 100644 --- a/apps/student/src/main/java/com/instructure/student/interfaces/ModuleAdapterToFragmentCallback.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleAdapterToFragmentCallback.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . * */ -package com.instructure.student.interfaces +package com.instructure.student.features.modules.list.adapter import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.models.ModuleObject diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleEmptyViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleEmptyViewHolder.kt new file mode 100644 index 0000000000..fa85adc92e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleEmptyViewHolder.kt @@ -0,0 +1,16 @@ +package com.instructure.student.features.modules.list.adapter + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.instructure.student.R +import com.instructure.student.databinding.ViewholderModuleEmptyBinding + +class ModuleEmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun bind(text: String?) = with(ViewholderModuleEmptyBinding.bind(itemView)){ + titleText.text = text + } + + companion object { + const val HOLDER_RES_ID = R.layout.viewholder_module_empty + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/holders/ModuleHeaderViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleHeaderViewHolder.kt similarity index 75% rename from apps/student/src/main/java/com/instructure/student/holders/ModuleHeaderViewHolder.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleHeaderViewHolder.kt index 6e508eecda..b66dd5b3fe 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/ModuleHeaderViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleHeaderViewHolder.kt @@ -1,20 +1,4 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.holders +package com.instructure.student.features.modules.list.adapter import android.animation.Animator import android.animation.AnimatorInflater @@ -30,7 +14,7 @@ import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.student.R import com.instructure.student.databinding.ViewholderHeaderModuleBinding -import com.instructure.student.util.ModuleUtility +import com.instructure.student.features.modules.util.ModuleUtility class ModuleHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var isExpanded: Boolean = false @@ -56,7 +40,10 @@ class ModuleHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) divider.setGone() } isExpanded = !isExpanded - val flipAnimator = AnimatorInflater.loadAnimator(v.context, animationType) as ObjectAnimator + val flipAnimator = AnimatorInflater.loadAnimator( + v.context, + animationType + ) as ObjectAnimator flipAnimator.target = expandCollapse flipAnimator.duration = 200 flipAnimator.start() @@ -90,10 +77,15 @@ class ModuleHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) } else { if (isLocked) R.drawable.ic_lock else R.drawable.ic_module_circle } - moduleStatus.setImageDrawable(ColorUtils.colorIt(color, ContextCompat.getDrawable(context, drawable)!!)) + moduleStatus.setImageDrawable( + ColorUtils.colorIt( + color, + ContextCompat.getDrawable(context, drawable)!! + ) + ) } companion object { const val HOLDER_RES_ID = R.layout.viewholder_header_module } -} +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt new file mode 100644 index 0000000000..1d1b24a4e3 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleListRecyclerAdapter.kt @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2016 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.modules.list.adapter + +import android.app.ProgressDialog +import android.content.Context +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.ColorDrawable +import android.os.CountDownTimer +import android.view.View +import android.view.WindowManager +import android.widget.ProgressBar +import androidx.recyclerview.widget.RecyclerView +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.ApiType +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.DateHelper +import com.instructure.canvasapi2.utils.Failure +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.pandarecycler.interfaces.ViewHolderHeaderClicked +import com.instructure.pandarecycler.util.GroupSortedList +import com.instructure.pandarecycler.util.Types +import com.instructure.pandautils.utils.Utils +import com.instructure.pandautils.utils.orDefault +import com.instructure.pandautils.utils.textAndIconColor +import com.instructure.student.R +import com.instructure.student.adapter.ExpandableRecyclerAdapter +import com.instructure.student.features.modules.list.CollapsedModulesStore +import com.instructure.student.features.modules.list.ModuleListRepository +import com.instructure.student.features.modules.util.ModuleUtility +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.util.Locale +import java.util.UUID + +open class ModuleListRecyclerAdapter( + private val courseContext: CanvasContext, + context: Context, + private var shouldExhaustPagination: Boolean, + private val repository: ModuleListRepository, + private val lifecycleScope: CoroutineScope, + private val adapterToFragmentCallback: ModuleAdapterToFragmentCallback? +) : ExpandableRecyclerAdapter(context, ModuleObject::class.java, ModuleItem::class.java) { + + private var initialDataJob: Job? = null + private var moduleObjectJob: Job? = null + + private val moduleFromNetworkOrDb = HashMap() + private var courseSettings: CourseSettings? = null + + /* For testing purposes only */ + protected constructor(context: Context, repository: ModuleListRepository, lifecycleScope: CoroutineScope) : this(CanvasContext.defaultCanvasContext(), context, false, repository, lifecycleScope, null) // Callback not needed for testing, cast to null + + init { + viewHolderHeaderClicked = object : ViewHolderHeaderClicked { + override fun viewClicked(view: View?, moduleObject: ModuleObject) { + val isFromNetworkOrDb = moduleFromNetworkOrDb[moduleObject.id] ?: false + if (!isFromNetworkOrDb && !isGroupExpanded(moduleObject)) { + lifecycleScope.launch { + val result = repository.getFirstPageModuleItems(courseContext, moduleObject.id, true) + handleModuleItemResponse(result, moduleObject, false) + } + } else { + CollapsedModulesStore.markModuleCollapsed(courseContext, moduleObject.id, true) + expandCollapseGroup(moduleObject) + } + } + + } + isExpandedByDefault = false + isDisplayEmptyCell = true + if (adapterToFragmentCallback != null) loadData() // Callback is null when testing + } + + override fun createViewHolder(v: View, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + Types.TYPE_HEADER -> ModuleHeaderViewHolder(v) + Types.TYPE_SUB_HEADER -> ModuleSubHeaderViewHolder(v) + Types.TYPE_EMPTY_CELL -> ModuleEmptyViewHolder(v) + else -> ModuleViewHolder(v) + } + } + + override fun onBindChildHolder(holder: RecyclerView.ViewHolder, moduleObject: ModuleObject, moduleItem: ModuleItem) { + if (holder is ModuleSubHeaderViewHolder) { + val groupItemCount = getGroupItemCount(moduleObject) + val itemPosition = storedIndexOfItem(moduleObject, moduleItem) + holder.bind(moduleItem, itemPosition == 0, itemPosition == groupItemCount - 1) + } else { + val courseColor = courseContext.textAndIconColor + val groupItemCount = getGroupItemCount(moduleObject) + val itemPosition = storedIndexOfItem(moduleObject, moduleItem) + + (holder as ModuleViewHolder).bind(moduleObject, moduleItem, context, adapterToFragmentCallback, courseColor, + itemPosition == 0, itemPosition == groupItemCount - 1, courseSettings?.restrictQuantitativeData.orDefault()) + } + } + + override fun onBindHeaderHolder(holder: RecyclerView.ViewHolder, moduleObject: ModuleObject, isExpanded: Boolean) { + (holder as ModuleHeaderViewHolder).bind(moduleObject, context, viewHolderHeaderClicked, isExpanded) + } + + override fun onBindEmptyHolder(holder: RecyclerView.ViewHolder, moduleObject: ModuleObject) { + val moduleEmptyViewHolder = holder as ModuleEmptyViewHolder + // Keep displaying No connection as long as the result is not from network + // Doing so will cause the user to toggle the expand to refresh the list, if they had expanded a module while offline + if (moduleFromNetworkOrDb.containsKey(moduleObject.id) && moduleFromNetworkOrDb[moduleObject.id] == true) { + moduleEmptyViewHolder.bind(getPrerequisiteString(moduleObject)) + } else { + moduleEmptyViewHolder.bind(context.getString(R.string.noConnection)) + } + } + + override fun itemLayoutResId(viewType: Int): Int { + return when (viewType) { + Types.TYPE_HEADER -> ModuleHeaderViewHolder.HOLDER_RES_ID + Types.TYPE_SUB_HEADER -> ModuleSubHeaderViewHolder.HOLDER_RES_ID + Types.TYPE_EMPTY_CELL -> ModuleEmptyViewHolder.HOLDER_RES_ID + else -> ModuleViewHolder.HOLDER_RES_ID + } + } + + override fun refresh() { + shouldExhaustPagination = false + moduleFromNetworkOrDb.clear() + initialDataJob?.cancel() + collapseAll() + super.refresh() + } + + // region Expandable Callbacks + override fun createGroupCallback(): GroupSortedList.GroupComparatorCallback { + return object : GroupSortedList.GroupComparatorCallback { + override fun compare(o1: ModuleObject, o2: ModuleObject): Int = o1.position - o2.position + override fun areContentsTheSame(oldGroup: ModuleObject, newGroup: ModuleObject): Boolean { + val isNewLocked = ModuleUtility.isGroupLocked(newGroup) + val isOldLocked = ModuleUtility.isGroupLocked(oldGroup) + return oldGroup.name == newGroup.name && isNewLocked == isOldLocked + } + + override fun areItemsTheSame(group1: ModuleObject, group2: ModuleObject): Boolean = group1.id == group2.id + override fun getGroupType(group: ModuleObject): Int = Types.TYPE_HEADER + override fun getUniqueGroupId(group: ModuleObject): Long = group.id + } + } + + override fun createItemCallback(): GroupSortedList.ItemComparatorCallback { + return object : GroupSortedList.ItemComparatorCallback { + override fun compare(group: ModuleObject, o1: ModuleItem, o2: ModuleItem): Int = o1.position - o2.position + override fun areContentsTheSame(oldItem: ModuleItem, newItem: ModuleItem): Boolean = oldItem.title == newItem.title + override fun areItemsTheSame(item1: ModuleItem, item2: ModuleItem): Boolean = item1.id == item2.id + + override fun getChildType(group: ModuleObject, item: ModuleItem): Int { + return if (item.type == ModuleItem.Type.SubHeader.toString()) { + Types.TYPE_SUB_HEADER + } else Types.TYPE_ITEM + } + + override fun getUniqueItemId(item: ModuleItem): Long = item.id + } + } + // endregion + + + private fun createProgressDialog(context: Context): ProgressDialog { + val dialog = ProgressDialog(context) + try { + dialog.show() + } catch (e: WindowManager.BadTokenException) { + } + + dialog.setCancelable(false) + + dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + dialog.setContentView(R.layout.progress_dialog) + val currentColor = courseContext.textAndIconColor + + (dialog.findViewById(R.id.progressBar) as ProgressBar).indeterminateDrawable.setColorFilter(currentColor, PorterDuff.Mode.SRC_ATOP) + return dialog + } + + fun updateWithoutResettingViews(moduleObject: ModuleObject) { + moduleFromNetworkOrDb.clear() + moduleObjectJob?.cancel() + + lifecycleScope.launch { + val result = repository.getFirstPageModuleItems(courseContext, moduleObject.id, true) + handleModuleItemResponse(result, moduleObject, false) + } + } + + fun updateMasteryPathItems() { + + val dialog = createProgressDialog(context) + dialog.show() + // Show for 3 seconds and then refresh the list + // This 3 seconds is to allow the Canvas database to update so we can pull the module info down + object : CountDownTimer(3000, 1000) { + + override fun onTick(millisUntilFinished: Long) {} + + override fun onFinish() { + dialog.cancel() + refresh() + } + }.start() + } + + private fun handleModuleItemResponse(result: DataResult>, moduleObject: ModuleObject, isNotifyGroupChange: Boolean) { + if (result is DataResult.Success) { + val resultIsFromApiOrDb = result.apiType === ApiType.API || result.apiType == ApiType.DB + val moduleItems = result.data + + var position = if (moduleItems.isNotEmpty()) moduleItems[0].position - 1 else 0 + for (item in moduleItems) { + item.position = position++ + addOrUpdateItem(moduleObject, item) + position = if (resultIsFromApiOrDb) checkMasteryPaths(position, item, moduleObject) else position + } + + val nextItemsURL = result.linkHeaders.nextUrl + if (nextItemsURL != null) { + lifecycleScope.launch { + val nextPageResult = repository.getNextPageModuleItems(nextItemsURL, true) + handleModuleItemResponse(nextPageResult, moduleObject, isNotifyGroupChange) + } + } + + if (resultIsFromApiOrDb) { + moduleFromNetworkOrDb.put(moduleObject.id, true) + expandGroup(moduleObject, isNotifyGroupChange) + } else if (result.apiType === ApiType.CACHE) { + // Wait for the network to expand when there are no items + if (moduleItems.isNotEmpty()) { + expandGroup(moduleObject, isNotifyGroupChange) + } + } + CollapsedModulesStore.markModuleCollapsed(courseContext, moduleObject.id, false) + } else { + // Only expand if there was no cache result and no network. No connection empty cell will be displayed + val failedResult = result as DataResult.Fail + val errorCode = (failedResult.failure as? Failure.Network)?.errorCode + if (failedResult.response != null + && errorCode == 504 + && APIHelper.isCachedResponse(failedResult.response!!) + && !Utils.isNetworkAvailable(context)) { + expandGroup(moduleObject, isNotifyGroupChange) + } + } + } + + private fun checkMasteryPaths(initPosition: Int, item: ModuleItem, moduleObject: ModuleObject): Int { + var position = initPosition + if (item.masteryPaths != null && item.masteryPaths!!.isLocked) { + // Add another module item that says it's locked + val masteryPathsLocked = ModuleItem( + // Set an id so that if there is more than one path we'll display all of them. otherwise addOrUpdateItem will overwrite it + id = UUID.randomUUID().leastSignificantBits, + title = String.format(Locale.getDefault(), context.getString(R.string.locked_mastery_paths), item.title), + type = ModuleItem.Type.Locked.toString(), + completionRequirement = null, + position = position++ + ) + addOrUpdateItem(moduleObject, masteryPathsLocked) + } else if (item.masteryPaths != null && !item.masteryPaths!!.isLocked && item.masteryPaths!!.selectedSetId == 0L) { + // Add another module item that says select to choose assignment group + // We only want to do this when we have a mastery paths object, it's unlocked, and the user hasn't already selected a set + val masteryPathsSelect = ModuleItem( + // Set an id so that if there is more than one path we'll display all of them. otherwise addOrUpdateItem will overwrite it + id = UUID.randomUUID().leastSignificantBits, + title = context.getString(R.string.choose_assignment_group), + type = ModuleItem.Type.ChooseAssignmentGroup.toString(), + completionRequirement = null, + position = position++ + ) + + // Sort the mastery paths by position + item.masteryPaths!!.assignmentSets!!.sortBy { it?.position } + masteryPathsSelect.masteryPathsItemId = item.id + masteryPathsSelect.masteryPaths = item.masteryPaths + addOrUpdateItem(moduleObject, masteryPathsSelect) + notifyDataSetChanged() + } + return position + } + + // region Pagination + override val isPaginated get() = true + + private fun handleModuleObjectsResponse(result: DataResult>) { + if (result is DataResult.Success) { + val moduleObjects = result.data + setNextUrl(result.linkHeaders.nextUrl) + val collapsedItems = CollapsedModulesStore.getCollapsedModuleIds(courseContext) + moduleObjects.toTypedArray().forEach { + addOrUpdateGroup(it) + if (!collapsedItems.contains(it.id)) { + lifecycleScope.launch { + val itemsResult = repository.getFirstPageModuleItems(courseContext, it.id, true) + handleModuleItemResponse(itemsResult, it, true) + } + } + } + if(!shouldExhaustPagination || result.linkHeaders.nextUrl == null) { + // If we should exhaust pagination wait until we are done exhausting pagination + adapterToFragmentCallback?.onRefreshFinished() + } + this@ModuleListRecyclerAdapter.onCallbackFinished(result.apiType) + } else { + this@ModuleListRecyclerAdapter.onCallbackFinished(ApiType.API) // We can only get failed data result when it comes from the API + } + } + + override fun loadFirstPage() { + initialDataJob = lifecycleScope.tryLaunch { + val tabs = repository.getTabs(courseContext, isRefresh) + courseSettings = repository.loadCourseSettings(courseContext.id, isRefresh) + + // We only want to show modules if its a course nav option OR set to as the homepage + if (tabs.find { it.tabId == "modules" } != null || (courseContext as Course).homePage?.apiString == "modules") { + moduleObjectJob = lifecycleScope.launch { + val result = if (shouldExhaustPagination) { + repository.getAllModuleObjects(courseContext, true) + } else { + repository.getFirstPageModuleObjects(courseContext, true) + } + handleModuleObjectsResponse(result) + } + } else { + adapterToFragmentCallback?.onRefreshFinished(true) + } + } catch { + adapterToFragmentCallback?.onRefreshFinished(true) + } + } + + override fun loadNextPage(nextURL: String) { + moduleObjectJob = lifecycleScope.launch { + val result = repository.getNextPageModuleObjects(nextURL, true) + handleModuleObjectsResponse(result) + } + } + + // endregion + + // region Module binder Helper + // never actually shows prereqs because grayed out module items show instead. + private fun getPrerequisiteString(moduleObject: ModuleObject): String { + var prereqString = context.getString(R.string.noItemsToDisplayShort) + + if (ModuleUtility.isGroupLocked(moduleObject)) { + prereqString = context.getString(R.string.locked) + } + + if (moduleObject.state != null && + moduleObject.state == ModuleObject.State.Locked.apiString && + getGroupItemCount(moduleObject) > 0 && + getItem(moduleObject, 0)?.type == ModuleObject.State.UnlockRequirements.apiString) { + + val reqs = StringBuilder() + val ids = moduleObject.prerequisiteIds + //check to see if they need to finish other modules first + if (ids != null) { + for (i in ids.indices) { + val prereqModuleObject = getGroup(ids[i]) + if (prereqModuleObject != null) { + if (i == 0) { //if it's the first one, add the "Prerequisite:" label + reqs.append(context.getString(R.string.prerequisites) + " " + prereqModuleObject.name) + } else { + reqs.append(", " + prereqModuleObject.name!!) + } + } + } + } + + if (moduleObject.unlockAt != null) { + //only want a newline if there are prerequisite ids + if (ids!!.size > 0 && ids[0] != 0L) { + reqs.append("\n") + } + reqs.append(DateHelper.createPrefixedDateTimeString(context, R.string.unlocked, moduleObject.unlockDate)) + } + + prereqString = reqs.toString() + } + return prereqString + } + // endregion +} diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleSubHeaderViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleSubHeaderViewHolder.kt new file mode 100644 index 0000000000..03fc53e8db --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleSubHeaderViewHolder.kt @@ -0,0 +1,23 @@ +package com.instructure.student.features.modules.list.adapter + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.student.R +import com.instructure.student.databinding.ViewholderSubHeaderModuleBinding +import com.instructure.student.util.BinderUtils + +class ModuleSubHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun bind(moduleItem: ModuleItem, isFirstItem: Boolean, isLastItem: Boolean) = with( + ViewholderSubHeaderModuleBinding.bind(itemView) + ) { + if (ModuleItem.Type.SubHeader.toString().equals(moduleItem.type, ignoreCase = true)) { + subTitle.text = moduleItem.title + } + BinderUtils.updateShadows(isFirstItem, isLastItem, shadowTop, shadowBottom) + } + + companion object { + const val HOLDER_RES_ID = R.layout.viewholder_sub_header_module + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/holders/ModuleViewHolder.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt similarity index 82% rename from apps/student/src/main/java/com/instructure/student/holders/ModuleViewHolder.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt index 28c299efd5..f95fdb3ce7 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/ModuleViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/adapter/ModuleViewHolder.kt @@ -1,20 +1,4 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.holders +package com.instructure.student.features.modules.list.adapter import android.content.Context import android.graphics.Typeface @@ -26,12 +10,16 @@ import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.canvasapi2.utils.isValid -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.DP +import com.instructure.pandautils.utils.setGone +import com.instructure.pandautils.utils.setInvisible +import com.instructure.pandautils.utils.setTextForVisibility +import com.instructure.pandautils.utils.setVisible import com.instructure.student.R import com.instructure.student.databinding.ViewholderModuleBinding -import com.instructure.student.interfaces.ModuleAdapterToFragmentCallback +import com.instructure.student.features.modules.util.ModuleUtility import com.instructure.student.util.BinderUtils -import com.instructure.student.util.ModuleUtility private const val MODULE_INDENT_IN_DP = 10 @@ -78,16 +66,24 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { when (ModuleObject.State.values().firstOrNull { it.apiString == requirement.type }) { ModuleObject.State.MustSubmit -> { if (complete) description.setTextColor(courseColor) - if (complete) context.getString(R.string.moduleItemSubmitted) else context.getString(R.string.moduleItemSubmit) + if (complete) context.getString(R.string.moduleItemSubmitted) else context.getString( + R.string.moduleItemSubmit + ) } ModuleObject.State.MustView -> { - if (complete) context.getString(R.string.moduleItemViewed) else context.getString(R.string.moduleItemMustView) + if (complete) context.getString(R.string.moduleItemViewed) else context.getString( + R.string.moduleItemMustView + ) } ModuleObject.State.MustContribute -> { - if (complete) context.getString(R.string.moduleItemContributed) else context.getString(R.string.moduleItemContribute) + if (complete) context.getString(R.string.moduleItemContributed) else context.getString( + R.string.moduleItemContribute + ) } ModuleObject.State.MinScore -> { - if (complete) context.getString(R.string.moduleItemMinScoreMet) else context.getString(R.string.moduleItemMinScore) + " " + requirement.minScore + if (complete) context.getString(R.string.moduleItemMinScoreMet) else context.getString( + R.string.moduleItemMinScore + ) + " " + requirement.minScore } else -> null } @@ -100,7 +96,8 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // Indicator indicator.setGone() if (moduleItem.completionRequirement?.completed == true) { - val drawable = ColorKeeper.getColoredDrawable(context, R.drawable.ic_check_white_24dp, courseColor) + val drawable = + ColorKeeper.getColoredDrawable(context, R.drawable.ic_check_white_24dp, courseColor) indicator.setImageDrawable(drawable) indicator.setVisible() } @@ -140,7 +137,11 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val hasDate: Boolean val hasPoints: Boolean if (details.dueDate != null) { - date.text = DateHelper.createPrefixedDateTimeString(context, R.string.toDoDue, details.dueDate) + date.text = DateHelper.createPrefixedDateTimeString( + context, + R.string.toDoDue, + details.dueDate + ) hasDate = true } else { date.text = "" @@ -176,4 +177,4 @@ class ModuleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { companion object { const val HOLDER_RES_ID = R.layout.viewholder_module } -} +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt new file mode 100644 index 0000000000..1826dea7f7 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListDataSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult + +interface ModuleListDataSource { + + suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> + + suspend fun getFirstPageModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> + + suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> + + suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> + + suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt new file mode 100644 index 0000000000..1a06c1c859 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSource.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.ApiType +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.facade.ModuleFacade + +class ModuleListLocalDataSource(private val tabDao: TabDao, private val moduleFacade: ModuleFacade, private val courseSettingsDao: CourseSettingsDao) : ModuleListDataSource { + + override suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val moduleObjects = moduleFacade.getModuleObjects(canvasContext.id) + return DataResult.Success(moduleObjects, apiType = ApiType.DB) + } + + override suspend fun getFirstPageModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val moduleObjects = moduleFacade.getModuleObjects(canvasContext.id) + return DataResult.Success(moduleObjects, apiType = ApiType.DB) + } + + override suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return DataResult.Success(tabDao.findByCourseId(canvasContext.id).map { it.toApiModel() }, apiType = ApiType.DB) + } + + override suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> { + val moduleItems = moduleFacade.getModuleItems(moduleId) + return DataResult.Success(moduleItems, apiType = ApiType.DB) + } + + override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return courseSettingsDao.findByCourseId(courseId)?.toApiModel() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt new file mode 100644 index 0000000000..76e6b84a65 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSource.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class ModuleListNetworkDataSource( + private val moduleApi: ModuleAPI.ModuleInterface, + private val tabApi: TabAPI.TabsInterface, + private val courseApi: CourseAPI.CoursesInterface) : ModuleListDataSource { + + override suspend fun getAllModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getFirstPageModuleObjects(canvasContext.apiContext(), canvasContext.id, params).depaginate { + moduleApi.getNextPageModuleObjectList(it, params) + } + } + + override suspend fun getFirstPageModuleObjects(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getFirstPageModuleObjects(canvasContext.apiContext(), canvasContext.id, params) + } + + suspend fun getNextPageModuleObjects(nextUrl: String, forceNetwork: Boolean): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getNextPageModuleObjectList(nextUrl, params) + } + + override suspend fun getTabs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return tabApi.getTabs(canvasContext.id, canvasContext.apiContext(), params) + } + + override suspend fun getFirstPageModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getFirstPageModuleItems(canvasContext.apiContext(), canvasContext.id, moduleId, params) + } + + suspend fun getNextPageModuleItems(nextUrl: String, forceNetwork: Boolean): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getNextPageModuleItemList(nextUrl, params) + } + + override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return courseApi.getCourseSettings(courseId, restParams).dataOrNull + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt similarity index 87% rename from apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt index ef94c02f2d..9bf146ebba 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.modules.progression import android.content.Context import android.net.Uri @@ -27,15 +27,13 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import com.instructure.canvasapi2.StatusCallback -import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.ModuleObject.State import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.interactions.FragmentInteractions import com.instructure.interactions.bookmarks.Bookmarkable @@ -52,17 +50,20 @@ import com.instructure.student.R import com.instructure.student.databinding.CourseModuleProgressionBinding import com.instructure.student.events.ModuleUpdatedEvent import com.instructure.student.events.post -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.files.details.FileDetailsFragment +import com.instructure.student.features.modules.list.ModuleListFragment +import com.instructure.student.features.modules.util.ModuleProgressionUtility +import com.instructure.student.features.modules.util.ModuleUtility +import com.instructure.student.features.pages.details.PageDetailsFragment +import com.instructure.student.fragment.BasicQuizViewFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const import com.instructure.student.util.CourseModulesStore -import com.instructure.student.util.ModuleProgressionUtility -import com.instructure.student.util.ModuleUtility import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import okhttp3.ResponseBody -import retrofit2.Response import javax.inject.Inject @PageView(url = "courses/{canvasContext}/modules") @@ -75,7 +76,9 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { @Inject lateinit var discussionRouteHelper: DiscussionRouteHelper - private var routeModuleProgressionJob: Job? = null + @Inject + lateinit var repository: ModuleProgressionRepository + private var moduleItemsJob: Job? = null private var markAsReadJob: WeaveJob? = null @@ -87,6 +90,8 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { private var items: ArrayList> by SerializableArg(key = MODULE_ITEMS, default = ArrayList()) private var assetId: String by StringArg(key = ASSET_ID) private var assetType: String by StringArg(key = ASSET_TYPE, default = ModuleItemAsset.MODULE_ITEM.assetType) + // This is used in offline cases when the modules are not synced, but we have links with moduleId. This will never be module item asset type. + private var secondaryAssetType: String? by NullableStringArg(key = SECONDARY_ASSET_TYPE) private var route: Route by ParcelableArg(key = ROUTE) private var navigatedFromModules: Boolean by BooleanArg(key = NAVIGATED_FROM_MODULES) @@ -102,6 +107,10 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { private var isDiscussionRedesignEnabled = false + private var snycedTabs = emptySet() + private var syncedFileIds = emptyList() + private var isOfflineEnabled = false + val tabId: String get() = Tab.MODULES_ID @@ -125,6 +134,9 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) lifecycleScope.launch { + isOfflineEnabled = repository.isOfflineEnabled() + snycedTabs = repository.getSyncedTabs(canvasContext.id) + syncedFileIds = repository.getSyncedFileIds(canvasContext.id) isDiscussionRedesignEnabled = discussionRouteHelper.isDiscussionRedesignEnabled(canvasContext) loadModuleProgression(savedInstanceState) } @@ -132,8 +144,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { override fun onDestroyView() { super.onDestroyView() - routeModuleProgressionJob?.cancel() - moduleItemsJob?.cancel() markAsReadJob?.cancel() } @@ -195,23 +205,17 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { binding.prevItem.setOnClickListener(prevItemClickCallback) binding.nextItem.setOnClickListener(nextItemClickCallback) - binding.markDoneButton.setOnClickListener { + binding.markDoneButton.onClickWithRequireNetwork { val moduleItem = getModelObject() if (moduleItem?.completionRequirement != null) { - if (moduleItem.completionRequirement!!.completed) { - ModuleManager.markAsNotDone(canvasContext, moduleItem.moduleId, moduleItem.id, - object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - setMarkDone(moduleItem, false) - } - }) - } else { - ModuleManager.markAsDone(canvasContext, moduleItem.moduleId, moduleItem.id, - object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - setMarkDone(moduleItem, true) - } - }) + lifecycleScope.launch { + if (moduleItem.completionRequirement!!.completed) { + val result = repository.markAsNotDone(canvasContext, moduleItem) + if (result.isSuccess) setMarkDone(moduleItem, false) + } else { + val result = repository.markAsDone(canvasContext, moduleItem) + if (result.isSuccess) setMarkDone(moduleItem, true) + } } } } @@ -307,18 +311,18 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { addLockedIconIfNeeded(modules, items, groupPos, childPos) // Mark the item as viewed - markAsRead(currentModuleItem.moduleId, currentModuleItem.id) + markAsRead(currentModuleItem) } updateModuleMarkDoneView(currentModuleItem) } - private fun markAsRead(moduleId: Long, moduleItemId: Long) { + private fun markAsRead(moduleItem: ModuleItem) { markAsReadJob = tryWeave { // mark the moduleItem as viewed if we have a valid module id and item id, // but not the files, because they need to open or download those to view them - if (moduleId != 0L && moduleItemId != 0L && getCurrentModuleItem(currentPos)!!.type != ModuleItem.Type.File.toString()) { - awaitApi { ModuleManager.markModuleItemAsRead(canvasContext, moduleId, moduleItemId, it) } + if (moduleItem.moduleId != 0L && moduleItem.id != 0L && getCurrentModuleItem(currentPos)!!.type != ModuleItem.Type.File.toString()) { + repository.markAsRead(canvasContext, moduleItem) // Update the module item locally, needed to unlock modules as the user ViewPages through them getCurrentModuleItem(currentPos)?.completionRequirement?.completed = true @@ -326,8 +330,8 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { setupNextModule(getModuleItemGroup(currentPos)) // Update the module state to indicate in the list that the module is completed - val module = modules.find { it.id == moduleId } ?: return@tryWeave - val isModuleCompleted = items.flatten().filter { it.moduleId == moduleId }.all { it.completionRequirement?.completed.orDefault() } + val module = modules.find { it.id == moduleItem.moduleId } ?: return@tryWeave + val isModuleCompleted = items.flatten().filter { it.moduleId == moduleItem.moduleId }.all { it.completionRequirement?.completed.orDefault() } val updatedState = if (isModuleCompleted) State.Completed.apiString else module.state // Update the module list fragment to show that these requirements are done, @@ -382,8 +386,8 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { } private fun getModuleItemData(moduleId: Long) { - moduleItemsJob = tryWeave { - val moduleItems = awaitApi> { ModuleManager.getAllModuleItems(canvasContext, moduleId, it, true) } + moduleItemsJob = lifecycleScope.tryLaunch { + val moduleItems = repository.getAllModuleItems(canvasContext, moduleId, true) // Update ui here with results // Holds the position of the module the current module item belongs to var index = 0 @@ -635,7 +639,17 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { // so we need to find the correct one overall val moduleItem = getCurrentModuleItem(position) ?: getCurrentModuleItem(0) // Default to the first item, band-aid for NPE - val fragment = ModuleUtility.getFragment(moduleItem!!, canvasContext as Course, modules[groupPos], isDiscussionRedesignEnabled, navigatedFromModules) + val fragment = ModuleUtility.getFragment( + moduleItem!!, + canvasContext as Course, + modules[groupPos], + isDiscussionRedesignEnabled, + navigatedFromModules, + repository.isOnline() || !isOfflineEnabled, // If the offline feature is disabled we always use the online behavior + snycedTabs, + syncedFileIds, + requireContext() + ) var args: Bundle? = fragment!!.arguments if (args == null) { args = Bundle() @@ -691,18 +705,18 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { } //endregion - private fun loadModuleProgression(bundle: Bundle?) = with(binding) { + private fun loadModuleProgression(bundle: Bundle?) { if(assetId.isBlank()) { - bottomBarModule.setVisible() + binding.bottomBarModule.setVisible() setViewInfo(bundle) setButtonListeners() updateBottomNavBarButtons() return } - progressBar.setVisible() - routeModuleProgressionJob = tryWeave { - val moduleItemSequence = awaitApi { ModuleManager.getModuleItemSequence(canvasContext, assetType, assetId, it, true) } + binding.progressBar.setVisible() + lifecycleScope.tryLaunch { + val moduleItemSequence = repository.getModuleItemSequence(canvasContext, assetType, assetId, true) // Make sure that there is a sequence val sequenceItems = moduleItemSequence.items ?: emptyArray() if (sequenceItems.isNotEmpty()) { @@ -711,7 +725,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { assetType == ModuleItemAsset.MODULE_ITEM.assetType -> sequenceItems.firstOrNull { it.current!!.id == assetId.toLong() }?.current ?: sequenceItems[0].current else -> sequenceItems[0].current } - val moduleItems = awaitApi> { ModuleManager.getAllModuleItems(canvasContext, current!!.moduleId, it, true) } + val moduleItems = repository.getAllModuleItems(canvasContext, current!!.moduleId, true) val unfilteredItems = ArrayList>(1).apply { add(ArrayList(moduleItems)) } modules = ArrayList(1).apply { moduleItemSequence.modules!!.firstOrNull { it.id == current?.moduleId }?.let { add(it) } } val moduleHelper = ModuleProgressionUtility.prepareModulesForCourseProgression(requireContext(), current!!.id, modules, unfilteredItems) @@ -719,21 +733,25 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { childPos = moduleHelper.newChildPosition items = moduleHelper.strippedModuleItems } else { - progressBar.setGone() - val moduleItemAsset = ModuleItemAsset.fromAssetType(assetType) - if (moduleItemAsset != ModuleItemAsset.MODULE_ITEM) { - val newRoute = route.copy(secondaryClass = moduleItemAsset.routeClass, removePreviousScreen = true) - RouteMatcher.route(requireContext(), newRoute) - return@tryWeave + binding.progressBar.setGone() + val moduleItemAsset = ModuleItemAsset.fromAssetType(assetType) ?: ModuleItemAsset.MODULE_ITEM + val secondaryItemAsset = ModuleItemAsset.fromAssetType(secondaryAssetType) + val routeOffline = !repository.isOnline() && isOfflineEnabled && secondaryItemAsset != null + + if (moduleItemAsset != ModuleItemAsset.MODULE_ITEM || routeOffline) { + // If the asset type is not a module item it means that the secondaryItemAsset will never be null. + val newRoute = route.copy(secondaryClass = secondaryItemAsset!!.routeClass, removePreviousScreen = true) + RouteMatcher.route(requireActivity(), newRoute) + return@tryLaunch } } - progressBar.setGone() - bottomBarModule.setVisible() + binding.progressBar.setGone() + binding.bottomBarModule.setVisible() setViewInfo(bundle) setButtonListeners() } catch { - progressBar.setGone() + binding.progressBar.setGone() Logger.e("Error routing modules: " + it.message) } } @@ -747,6 +765,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { private const val CHILD_POSITION = "child_position" private const val ASSET_ID = "asset_id" private const val ASSET_TYPE = "asset_type" + private const val SECONDARY_ASSET_TYPE = "secondary_asset_type" private const val ROUTE = "route" private const val NAVIGATED_FROM_MODULES = "navigated_from_modules" @@ -777,6 +796,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { } val asset = getAssetTypeAndId(route) assetType = asset.first.assetType + secondaryAssetType = getSecondaryAssetType(route)?.assetType assetId = asset.second } else null @@ -802,6 +822,18 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { return Pair(ModuleItemAsset.MODULE_ITEM, "") } + private fun getSecondaryAssetType(route: Route): ModuleItemAsset? { + val params = route.paramsHash + + ModuleItemAsset.values().forEach { + if (params.containsKey(it.assetIdParamName)) { + return it + } + } + + return null + } + private fun validRoute(route: Route): Boolean = route.canvasContext != null && (CourseModulesStore.moduleObjects != null && CourseModulesStore.moduleListItems != null) || route.queryParamsHash.keys.any { it == RouterParams.MODULE_ITEM_ID } @@ -820,6 +852,6 @@ enum class ModuleItemAsset(val assetType: String, val assetIdParamName: String, FILE("File", RouterParams.FILE_ID, FileDetailsFragment::class.java); companion object { - fun fromAssetType(assetType: String): ModuleItemAsset = values().find { it.assetType == assetType } ?: MODULE_ITEM + fun fromAssetType(assetType: String?): ModuleItemAsset? = values().find { it.assetType == assetType } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt similarity index 97% rename from apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt index 510a568fce..d708d01bd6 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/LockedModuleItemFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.modules.progression import android.os.Bundle import android.view.LayoutInflater @@ -37,6 +37,7 @@ import com.instructure.pandautils.utils.setupAsBackButton import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R import com.instructure.student.databinding.FragmentLockedModuleItemBinding +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher @ScreenView(SCREEN_VIEW_LOCKED_MODULE_ITEM) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/ModuleProgressionRepository.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/ModuleProgressionRepository.kt new file mode 100644 index 0000000000..46fc55e11d --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/ModuleProgressionRepository.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionDataSource +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionLocalDataSource +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionNetworkDataSource +import okhttp3.ResponseBody + +class ModuleProgressionRepository( + localDataSource: ModuleProgressionLocalDataSource, + private val networkDataSource: ModuleProgressionNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val localFileDao: LocalFileDao +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getAllModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): List { + return dataSource().getAllModuleItems(canvasContext, moduleId, forceNetwork) + } + + suspend fun getModuleItemSequence(canvasContext: CanvasContext, assetType: String, assetId: String, forceNetwork: Boolean): ModuleItemSequence { + return dataSource().getModuleItemSequence(canvasContext, assetType, assetId, forceNetwork) + } + + suspend fun getDetailedQuiz(url: String, quizId: Long, forceNetwork: Boolean): Quiz { + return dataSource().getDetailedQuiz(url, quizId, forceNetwork) + } + + suspend fun markAsNotDone(canvasContext: CanvasContext, moduleItem: ModuleItem): DataResult { + return networkDataSource.markAsNotDone(canvasContext, moduleItem) + } + + suspend fun markAsDone(canvasContext: CanvasContext, moduleItem: ModuleItem): DataResult { + return networkDataSource.markAsDone(canvasContext, moduleItem) + } + + suspend fun markAsRead(canvasContext: CanvasContext, moduleItem: ModuleItem): DataResult { + return networkDataSource.markAsRead(canvasContext, moduleItem) + } + + suspend fun getSyncedTabs(courseId: Long): Set { + val courseSyncSettings = courseSyncSettingsDao.findById(courseId) + return courseSyncSettings?.tabs?.filter { it.value }?.keys ?: emptySet() + } + + suspend fun getSyncedFileIds(courseId: Long): List { + val syncedFiles = localFileDao.findByCourseId(courseId) + return syncedFiles.map { it.id } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ModuleQuizDecider.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/ModuleQuizDecider.kt similarity index 85% rename from apps/student/src/main/java/com/instructure/student/fragment/ModuleQuizDecider.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/progression/ModuleQuizDecider.kt index cec1d182b6..20229f209a 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ModuleQuizDecider.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/ModuleQuizDecider.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.modules.progression import android.os.Bundle import android.view.LayoutInflater @@ -23,16 +23,15 @@ import android.view.View import android.view.ViewGroup import android.webkit.WebView import androidx.core.content.ContextCompat -import com.instructure.canvasapi2.managers.QuizManager +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.validOrNull import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_MODULE_QUIZ_DECIDER import com.instructure.pandautils.analytics.ScreenView @@ -41,16 +40,26 @@ import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R import com.instructure.student.databinding.FragmentModuleQuizDeciderBinding +import com.instructure.student.fragment.BasicQuizViewFragment +import com.instructure.student.fragment.InternalWebviewFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @ScreenView(SCREEN_VIEW_MODULE_QUIZ_DECIDER) +@AndroidEntryPoint class ModuleQuizDecider : ParentFragment() { private val binding by viewBinding(FragmentModuleQuizDeciderBinding::bind) + @Inject + lateinit var repository: ModuleProgressionRepository + private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) private var baseURL by StringArg(key = Const.URL) private var apiURL by StringArg(key = Const.API_URL) + private var quizId by LongArg(key = Const.ID) private lateinit var quiz: Quiz @@ -66,11 +75,11 @@ class ModuleQuizDecider : ParentFragment() { override fun onPageStartedCallback(webView: WebView, url: String) = Unit override fun canRouteInternallyDelegate(url: String): Boolean { - return RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, false) + return RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, false) } override fun routeInternallyCallback(url: String) { - RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, true) + RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, true) } } @@ -110,10 +119,10 @@ class ModuleQuizDecider : ParentFragment() { } private fun obtainQuiz() = with(binding) { - tryWeave { + lifecycleScope.tryLaunch { quizInfoContainer.setGone() progressBar.setVisible() - quiz = awaitApi { QuizManager.getDetailedQuizByUrl(apiURL, true, it) } + quiz = repository.getDetailedQuiz(apiURL, quizId, true) quizInfoContainer.setVisible() progressBar.setGone() quizTitle.text = quiz.title @@ -133,7 +142,6 @@ class ModuleQuizDecider : ParentFragment() { setupViews() } catch { toast(R.string.errorOccurred) - activity?.finish() } } @@ -141,19 +149,20 @@ class ModuleQuizDecider : ParentFragment() { binding.toolbar.title = quiz.title.validOrNull() ?: getString(R.string.quizzes) ViewStyler.themeButton(binding.goToQuiz) - binding.goToQuiz.onClick { + binding.goToQuiz.onClickWithRequireNetwork { val route = BasicQuizViewFragment.makeRoute(canvasContext, quiz, baseURL) route.ignoreDebounce = true - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } } companion object { - fun makeRoute(canvasContext: CanvasContext, url: String, apiURL: String): Route { + fun makeRoute(canvasContext: CanvasContext, url: String, apiURL: String, quizId: Long): Route { val bundle = Bundle().apply { putString(Const.URL, url) putString(Const.API_URL, apiURL) + putLong(Const.ID, quizId) } return Route(ModuleQuizDecider::class.java, canvasContext, bundle) } diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/NotAvailableOfflineFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/NotAvailableOfflineFragment.kt new file mode 100644 index 0000000000..643c08acf7 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/NotAvailableOfflineFragment.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.modules.progression + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.interactions.router.Route +import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.utils.* +import com.instructure.student.R +import com.instructure.student.databinding.FragmentNotAvailableOfflineBinding +import com.instructure.student.fragment.ParentFragment + +private const val MODULE_ITEM_NAME = "module_item_name" +private const val DESCRIPTION = "description" +private const val SHOW_TOOLBAR = "show-toolbar" + +class NotAvailableOfflineFragment : ParentFragment() { + + private var moduleItemName: String by StringArg(key = MODULE_ITEM_NAME) + private var description: String by StringArg(key = DESCRIPTION) + private var course: Course by ParcelableArg(key = Const.COURSE) + private var showToolbar: Boolean by BooleanArg(key = SHOW_TOOLBAR, default = true) + + private val binding by viewBinding(FragmentNotAvailableOfflineBinding::bind) + + //region Fragment Lifecycle Overrides + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + return inflater.inflate(R.layout.fragment_not_available_offline, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setIconVisibility() + binding.description.text = description + if (showToolbar) { + binding.toolbar.title = moduleItemName + binding.toolbar.setupAsBackButton(this) + ViewStyler.themeToolbarColored(requireActivity(), binding.toolbar, course) + } else { + binding.toolbar.setGone() + } + } + //endregion + + //region Fragment Interaction Overrides + override fun title(): String = moduleItemName + + override fun applyTheme() {} + //endregion + + companion object { + fun makeRoute(course: CanvasContext, moduleItemName: String? = null, description: String? = null, showToolbar: Boolean = true): Route { + val bundle = Bundle().apply { + putParcelable(Const.COURSE, course) + putString(MODULE_ITEM_NAME, moduleItemName.orEmpty()) + putString(DESCRIPTION, description.orEmpty()) + putBoolean(SHOW_TOOLBAR, showToolbar) + } + return Route(NotAvailableOfflineFragment::class.java, null, bundle) + } + + fun newInstance(route: Route) = NotAvailableOfflineFragment().apply { + arguments = route.arguments + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + setIconVisibility() + } + + private fun setIconVisibility() { + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + binding.notAvailableIcon.setVisible() + } else { + binding.notAvailableIcon.setGone() + } + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionDataSource.kt new file mode 100644 index 0000000000..60cacd8fb4 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionDataSource.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.Quiz + +interface ModuleProgressionDataSource { + + suspend fun getAllModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): List + + suspend fun getModuleItemSequence(canvasContext: CanvasContext, assetType: String, assetId: String, forceNetwork: Boolean): ModuleItemSequence + + suspend fun getDetailedQuiz(url: String, quizId: Long, forceNetwork: Boolean): Quiz +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSource.kt new file mode 100644 index 0000000000..350ea4a734 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSource.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleItemWrapper +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.student.features.modules.progression.ModuleItemAsset + +class ModuleProgressionLocalDataSource(private val moduleFacade: ModuleFacade, private val quizDao: QuizDao) : ModuleProgressionDataSource { + override suspend fun getAllModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): List { + return moduleFacade.getModuleItems(moduleId) + } + + override suspend fun getModuleItemSequence(canvasContext: CanvasContext, assetType: String, assetId: String, forceNetwork: Boolean): ModuleItemSequence { + val moduleItem = when (assetType) { + ModuleItemAsset.MODULE_ITEM.assetType -> { + moduleFacade.getModuleItemById(assetId.toLong()) + } + ModuleItemAsset.PAGE.assetType -> { + moduleFacade.getModuleItemForPage(assetId) + } + else -> { + moduleFacade.getModuleItemByAssetIdAndType(assetType, assetId.toLong()) + } + } + + if (moduleItem == null) return ModuleItemSequence() + + val moduleObject = moduleFacade.getModuleObjectById(moduleItem.moduleId) + val modules = if (moduleObject != null) arrayOf(moduleObject) else emptyArray() + + return ModuleItemSequence(arrayOf(ModuleItemWrapper(current = moduleItem)), modules) + } + + override suspend fun getDetailedQuiz(url: String, quizId: Long, forceNetwork: Boolean): Quiz { + val quiz = quizDao.findById(quizId)?.toApiModel() + return quiz ?: throw IllegalStateException("Quiz not found in database") + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSource.kt new file mode 100644 index 0000000000..018b2d771e --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSource.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression.datasource + +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate +import okhttp3.ResponseBody + +class ModuleProgressionNetworkDataSource(private val moduleApi: ModuleAPI.ModuleInterface, private val quizApi: QuizAPI.QuizInterface) : ModuleProgressionDataSource { + override suspend fun getAllModuleItems(canvasContext: CanvasContext, moduleId: Long, forceNetwork: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getFirstPageModuleItems(canvasContext.apiContext(), canvasContext.id, moduleId, params).depaginate { + moduleApi.getNextPageModuleItemList(it, params) + }.dataOrThrow + } + + override suspend fun getModuleItemSequence(canvasContext: CanvasContext, assetType: String, assetId: String, forceNetwork: Boolean): ModuleItemSequence { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return moduleApi.getModuleItemSequence(canvasContext.apiContext(), canvasContext.id, assetType, assetId, params).dataOrThrow + } + + override suspend fun getDetailedQuiz(url: String, quizId: Long, forceNetwork: Boolean): Quiz { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return quizApi.getDetailedQuizByUrl(url, params).dataOrThrow + } + + suspend fun markAsNotDone(canvasContext: CanvasContext, moduleItem: ModuleItem): DataResult { + return moduleApi.markModuleItemAsNotDone(canvasContext.apiContext(), canvasContext.id, moduleItem.moduleId, moduleItem.id, RestParams()) + } + + suspend fun markAsDone(canvasContext: CanvasContext, moduleItem: ModuleItem): DataResult { + return moduleApi.markModuleItemAsDone(canvasContext.apiContext(), canvasContext.id, moduleItem.moduleId, moduleItem.id, RestParams()) + } + + suspend fun markAsRead(canvasContext: CanvasContext, moduleItem: ModuleItem): DataResult { + return moduleApi.markModuleItemRead(canvasContext.apiContext(), canvasContext.id, moduleItem.moduleId, moduleItem.id, RestParams()) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/util/ModuleProgressionUtility.kt b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleProgressionUtility.kt similarity index 94% rename from apps/student/src/main/java/com/instructure/student/util/ModuleProgressionUtility.kt rename to apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleProgressionUtility.kt index dacf1b20fc..fa1d3c6343 100644 --- a/apps/student/src/main/java/com/instructure/student/util/ModuleProgressionUtility.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleProgressionUtility.kt @@ -14,13 +14,12 @@ * along with this program. If not, see . * */ -package com.instructure.student.util +package com.instructure.student.features.modules.util import android.content.Context import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.models.ModuleObject -import com.instructure.student.fragment.CourseModuleProgressionFragment.Companion.shouldAddModuleItem -import java.util.* +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment.Companion.shouldAddModuleItem object ModuleProgressionUtility { fun prepareModulesForCourseProgression( diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt new file mode 100644 index 0000000000..4a3bfe25ac --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.util + +import android.content.Context +import android.net.Uri +import androidx.fragment.app.Fragment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.APIHelper.expandTildeId +import com.instructure.canvasapi2.utils.findWithPrevious +import com.instructure.canvasapi2.utils.isLocked +import com.instructure.interactions.router.Route +import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment +import com.instructure.student.R +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment.Companion.makeRoute +import com.instructure.student.features.discussion.details.DiscussionDetailsFragment +import com.instructure.student.features.discussion.details.DiscussionDetailsFragment.Companion.makeRoute +import com.instructure.student.features.files.details.FileDetailsFragment +import com.instructure.student.features.modules.progression.LockedModuleItemFragment +import com.instructure.student.features.modules.progression.ModuleQuizDecider +import com.instructure.student.features.modules.progression.NotAvailableOfflineFragment +import com.instructure.student.features.pages.details.PageDetailsFragment +import com.instructure.student.fragment.InternalWebviewFragment +import com.instructure.student.fragment.InternalWebviewFragment.Companion.makeRoute +import com.instructure.student.fragment.MasteryPathSelectionFragment +import com.instructure.student.fragment.MasteryPathSelectionFragment.Companion.makeRoute +import java.util.Date + +object ModuleUtility { + fun getFragment( + item: ModuleItem, + course: Course, + moduleObject: ModuleObject?, + isDiscussionRedesignEnabled: Boolean, + navigatedFromModules: Boolean, + isOnline: Boolean, + syncedTabs: Set, + syncedFileIds: List, + context: Context + ): Fragment? = when (item.type) { + "Page" -> { + createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.PAGES_ID)) { + PageDetailsFragment.newInstance(PageDetailsFragment.makeRoute(course, item.title, item.pageUrl, navigatedFromModules)) + } + } + "Assignment" -> { + createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID, Tab.SYLLABUS_ID)) { + AssignmentDetailsFragment.newInstance(makeRoute(course, getAssignmentId(item))) + } + } + "Discussion" -> { + createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.DISCUSSIONS_ID)) { + if (isDiscussionRedesignEnabled && isOnline) { + DiscussionDetailsWebViewFragment.newInstance(getDiscussionRedesignRoute(item, course)) + } else { + DiscussionDetailsFragment.newInstance(getDiscussionRoute(item, course)) + } + } + } + "Locked" -> LockedModuleItemFragment.newInstance(LockedModuleItemFragment.makeRoute(course, item.title!!, item.moduleDetails?.lockExplanation ?: "")) + "SubHeader" -> null // Don't do anything with headers, they're just dividers so we don't show them here. + "Quiz" -> { + createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.QUIZZES_ID)) { + val apiURL = removeDomain(item.url) + ModuleQuizDecider.newInstance(ModuleQuizDecider.makeRoute(course, item.htmlUrl!!, apiURL!!, item.contentId)) + } + } + "ChooseAssignmentGroup" -> { + createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context) { + val route = makeRoute(course, item.masteryPaths!!, moduleObject!!.id, item.masteryPathsItemId) + MasteryPathSelectionFragment.newInstance(route) + } + } + "ExternalUrl", "ExternalTool" -> { + if (item.isLocked()) { + LockedModuleItemFragment.newInstance(LockedModuleItemFragment.makeRoute(course, item.title!!, item.moduleDetails?.lockExplanation ?: "")) + } else { + createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context) { + val uri = Uri.parse(item.htmlUrl).buildUpon().appendQueryParameter("display", "borderless").build() + val route = makeRoute(course, uri.toString(), item.title!!, true, true, true) + InternalWebviewFragment.newInstance(route) + } + } + } + "File" -> { // TODO Handle offline availability after files sync + createFileDetailsFragmentWithOfflineCheck(isOnline, course, item, moduleObject, syncedFileIds, context) + } + else -> null + } + + private fun createFragmentWithOfflineCheck( + isOnline: Boolean, + course: Course, + item: ModuleItem, + syncedTabs: Set, + context: Context, + tabs: Set = emptySet(), + creationBlock: () -> Fragment? + ): Fragment? { + return if (isOnline || tabs.any { syncedTabs.contains(it) }) { + creationBlock() + } else { + val descriptionResource = if (tabs.isEmpty()) R.string.notAvailableOfflineDescription else R.string.notAvailableOfflineDescriptionForTabs + NotAvailableOfflineFragment.newInstance(NotAvailableOfflineFragment.makeRoute(course, item.title, context.getString(descriptionResource))) + } + } + + private fun createFileDetailsFragmentWithOfflineCheck( + isOnline: Boolean, + course: Course, + item: ModuleItem, + moduleObject: ModuleObject?, + syncedFiles: List, + context: Context, + ): Fragment? { + return if (isOnline || syncedFiles.contains(item.contentId)) { + val url = removeDomain(item.url) + if (moduleObject == null) { + FileDetailsFragment.newInstance( + FileDetailsFragment.makeRoute( + course, + url!!, + item.contentId + ) + ) + } else { + FileDetailsFragment.newInstance( + FileDetailsFragment.makeRoute( + course, + moduleObject, + item.id, + url!!, + item.contentId + ) + ) + } + } else { + NotAvailableOfflineFragment.newInstance(NotAvailableOfflineFragment.makeRoute(course, item.title, context.getString(R.string.notAvailableOfflineDescriptionForTabs))) + } + } + + fun isGroupLocked(module: ModuleObject?): Boolean { + // NOTE: The state of the group is is "Locked" until the user visits the modules online + // Check if the unlock date has passed + if (module?.unlockDate?.after(Date()) == true) return true + + // Check if the state is Locked AND there are prerequisites + return module?.prerequisiteIds != null && module.state == ModuleObject.State.Locked.apiString + } + + private fun getAssignmentId(moduleItem: ModuleItem): Long { + // Get the assignment id from the url + return Uri.parse(moduleItem.url).pathSegments + .findWithPrevious { previous, _ -> previous == "assignments" } + ?.let { expandTildeId(it) } + ?.toLongOrNull() ?: 0 + } + + private fun getDiscussionRoute(moduleItem: ModuleItem, course: Course): Route { + // Get the topic id from the url + val topicId = Uri.parse(moduleItem.url).pathSegments + .findWithPrevious { previous, _ -> previous == "discussion_topics" } + ?.let { expandTildeId(it) } + ?.toLongOrNull() ?: 0 + return makeRoute(course, topicId, null) + } + + private fun getDiscussionRedesignRoute(moduleItem: ModuleItem, course: Course): Route { + // Get the topic id from the url + val topicId = Uri.parse(moduleItem.url).pathSegments + .findWithPrevious { previous, _ -> previous == "discussion_topics" } + ?.let { expandTildeId(it) } + ?.toLongOrNull() ?: 0 + return DiscussionDetailsWebViewFragment.makeRoute(course, topicId) + } + + /** Strips off the domain and protocol */ + private fun removeDomain(url: String?): String? = url?.substringAfter("/api/v1/") +} diff --git a/apps/student/src/main/java/com/instructure/student/features/navigation/NavigationRepository.kt b/apps/student/src/main/java/com/instructure/student/features/navigation/NavigationRepository.kt new file mode 100644 index 0000000000..2e8f1ecafb --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/navigation/NavigationRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.navigation + +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.navigation.datasource.NavigationDataSource +import com.instructure.student.features.navigation.datasource.NavigationLocalDataSource +import com.instructure.student.features.navigation.datasource.NavigationNetworkDataSource + +class NavigationRepository( + private val localDataSource: NavigationLocalDataSource, + private val networkDataSource: NavigationNetworkDataSource, + private val networkStateProvider: NetworkStateProvider, + private val featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getCourse(courseId: Long, forceNetwork: Boolean): Course? { + return dataSource().getCourse(courseId, forceNetwork) + } + + suspend fun isTokenValid(): Boolean { + try { + val result = networkDataSource.getSelf() + return result.isSuccess + } catch (e: Exception) { + return false + } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationDataSource.kt new file mode 100644 index 0000000000..8f04434254 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationDataSource.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.navigation.datasource + +import com.instructure.canvasapi2.models.* + +interface NavigationDataSource { + + suspend fun getCourse(courseId: Long, forceNetwork: Boolean): Course? +} diff --git a/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationLocalDataSource.kt new file mode 100644 index 0000000000..e519d661e4 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationLocalDataSource.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.navigation.datasource + +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade + +class NavigationLocalDataSource( + private val courseFacade: CourseFacade +) : NavigationDataSource { + + override suspend fun getCourse(courseId: Long, forceNetwork: Boolean): Course? { + return courseFacade.getCourseById(courseId) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationNetworkDataSource.kt new file mode 100644 index 0000000000..50afbc66ef --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/navigation/datasource/NavigationNetworkDataSource.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.navigation.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult + +class NavigationNetworkDataSource( + private val courseApi: CourseAPI.CoursesInterface, + private val userApi: UserAPI.UsersInterface, +) : NavigationDataSource { + + override suspend fun getCourse(courseId: Long, forceNetwork: Boolean): Course? { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return courseApi.getCourse(courseId, params).dataOrNull + } + + suspend fun getSelf(): DataResult { + val params = RestParams(isForceReadFromNetwork = true) + return userApi.getSelf(params) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/offline/sync/StudentSyncRouter.kt b/apps/student/src/main/java/com/instructure/student/features/offline/sync/StudentSyncRouter.kt new file mode 100644 index 0000000000..73fdc5d87f --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/offline/sync/StudentSyncRouter.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.offline.sync + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.offline.sync.SyncRouter +import com.instructure.pandautils.models.PushNotification +import com.instructure.pandautils.utils.Const +import com.instructure.student.activity.NavigationActivity + +class StudentSyncRouter : SyncRouter { + override fun routeToSyncProgress(context: Context): PendingIntent { + val path = "${ApiPrefs.fullDomain}/syncProgress" + val intent = Intent(context, NavigationActivity.startActivityClass).apply { + putExtra(Const.LOCAL_NOTIFICATION, true) + putExtra(PushNotification.HTML_URL, path) + } + + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt similarity index 87% rename from apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt index f938d788c6..ae55c2f3ef 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PageDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsFragment.kt @@ -14,21 +14,19 @@ * along with this program. If not, see . * */ -package com.instructure.student.fragment +package com.instructure.student.features.pages.details import android.os.Bundle import android.view.MenuItem import android.view.View import android.webkit.WebView +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.managers.OAuthManager -import com.instructure.canvasapi2.managers.PageManager import com.instructure.canvasapi2.models.AuthenticatedSession import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Page -import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.Logger +import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.BeforePageView import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.pageview.PageViewUrl @@ -44,19 +42,26 @@ import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R import com.instructure.student.events.PageUpdatedEvent +import com.instructure.student.fragment.EditPageDetailsFragment +import com.instructure.student.fragment.InternalWebviewFragment +import com.instructure.student.fragment.LtiLaunchFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.LockInfoHTMLHelper +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import org.greenrobot.eventbus.Subscribe -import retrofit2.Response import java.util.* import java.util.regex.Pattern +import javax.inject.Inject @ScreenView(SCREEN_VIEW_PAGE_DETAILS) @PageView +@AndroidEntryPoint class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { - private var fetchDataJob: WeaveJob? = null + @Inject + lateinit var repository: PageDetailsRepository + private var loadHtmlJob: Job? = null private var pageName: String? by NullableStringArg(key = PAGE_NAME) private var page: Page by ParcelableArg(default = Page(), key = PAGE) @@ -82,6 +87,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + retainInstance = false setShouldLoadUrl(false) } @@ -97,7 +103,6 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { override fun onDestroyView() { super.onDestroyView() - fetchDataJob?.cancel() loadHtmlJob?.cancel() } @@ -105,7 +110,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { super.onViewCreated(view, savedInstanceState) getCanvasWebView()?.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true - override fun launchInternalWebViewFragment(url: String) = InternalWebviewFragment.loadInternalWebView(activity, InternalWebviewFragment.makeRoute(canvasContext, url, isLTITool)) + override fun launchInternalWebViewFragment(url: String) = loadInternalWebView(activity, makeRoute(canvasContext, url, isLTITool)) } // Add to the webview client for clearing webview history after an update to prevent going back to old data @@ -127,9 +132,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_edit -> { - openEditPage(page) - } + R.id.menu_edit -> activity?.withRequireNetwork { openEditPage(page) } } return super.onOptionsItemSelected(item) } @@ -154,13 +157,14 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { } private fun fetchFontPage() { - fetchDataJob = tryWeave { - val response = awaitApiResponse { PageManager.getFrontPage(canvasContext, true, it) } - response.body()?.let { + lifecycleScope.tryLaunch { + val result = repository.getFrontPage(canvasContext, true) + result.onSuccess { nonNullArgs.putParcelable(PAGE, it) loadPage(it) + }.onFailure { + loadFailedPageInfo((it as? Failure.Network)?.errorCode) } - if (response.body() == null) loadFailedPageInfo(response) } catch { Logger.e("Page Fetch Error ${it.message}") loadFailedPageInfo() @@ -168,14 +172,15 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { } private fun fetchPageDetails() { - fetchDataJob = tryWeave { + lifecycleScope.tryLaunch { val pageUrl = pageUrl ?: page.url ?: pageName ?: throw Exception("Page url/name null!") - val response = awaitApiResponse { PageManager.getPageDetails(canvasContext, pageUrl, true, it) } - response.body()?.let { + val result = repository.getPageDetails(canvasContext, pageUrl, true) + result.onSuccess { nonNullArgs.putParcelable(PAGE, it) loadPage(it) + }.onFailure { + loadFailedPageInfo((it as? Failure.Network)?.errorCode) } - if (response.body() == null) loadFailedPageInfo(response) } catch { Logger.e("Page Fetch Error ${it.message}") loadFailedPageInfo() @@ -185,8 +190,8 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { private fun loadPage(page: Page) = with(binding) { setPageObject(page) - if (page.lockInfo != null) { - val lockedMessage = LockInfoHTMLHelper.getLockedInfoHTML(page.lockInfo!!, requireContext(), R.string.lockedPageDesc, !navigatedFromModules) + page.lockInfo?.let { + val lockedMessage = LockInfoHTMLHelper.getLockedInfoHTML(it, requireContext(), R.string.lockedPageDesc, !navigatedFromModules) populateWebView(lockedMessage, getString(R.string.pages)) return } @@ -204,7 +209,7 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { loadHtmlJob = canvasWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), body, { canvasWebViewWrapper.loadHtml(it, page.title, baseUrl = page.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } } else if (page.body == null || page.body?.endsWith("") == true) { loadHtml(resources.getString(R.string.noPageFound), "text/html", "utf-8", null) @@ -243,8 +248,8 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { return newHtml } - private fun loadFailedPageInfo(response: Response? = null) { - if (response != null && response.code() >= 400 && response.code() < 500 && pageName != null && pageName == Page.FRONT_PAGE_NAME) { + private fun loadFailedPageInfo(errorCode: Int? = null) { + if (errorCode != null && errorCode >= 400 && errorCode < 500 && pageName != null && pageName == Page.FRONT_PAGE_NAME) { var context: String = if (canvasContext.type == CanvasContext.Type.COURSE) { getString(R.string.course) @@ -278,12 +283,13 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { } override val bookmark: Bookmarker - get() = Bookmarker(true, canvasContext).withParam(RouterParams.PAGE_ID, if (Page.FRONT_PAGE_NAME == pageName) Page.FRONT_PAGE_NAME else pageName!!) + get() = Bookmarker(true, canvasContext) + .withParam(RouterParams.PAGE_ID, if (Page.FRONT_PAGE_NAME == pageName) Page.FRONT_PAGE_NAME else pageName!!) private fun openEditPage(page: Page) { if (APIHelper.hasNetworkConnection()) { val route = EditPageDetailsFragment.makeRoute(canvasContext, page) - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } @@ -344,7 +350,9 @@ class PageDetailsFragment : InternalWebviewFragment(), Bookmarkable { } fun makeRoute(canvasContext: CanvasContext, pageName: String?): Route { - return Route(null, PageDetailsFragment::class.java, canvasContext, canvasContext.makeBundle(Bundle().apply { if (pageName != null) putString(PAGE_NAME, pageName) })) + return Route(null, PageDetailsFragment::class.java, canvasContext, canvasContext.makeBundle(Bundle().apply { + if (pageName != null) putString(PAGE_NAME, pageName) + })) } fun makeRoute(canvasContext: CanvasContext, pageName: String?, pageUrl: String?, navigatedFromModules: Boolean): Route { diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsRepository.kt new file mode 100644 index 0000000000..63cf9edfd2 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/PageDetailsRepository.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.pages.details.datasource.PageDetailsDataSource +import com.instructure.student.features.pages.details.datasource.PageDetailsLocalDataSource +import com.instructure.student.features.pages.details.datasource.PageDetailsNetworkDataSource + +class PageDetailsRepository( + localDataSource: PageDetailsLocalDataSource, + networkDataSource: PageDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult { + return dataSource().getFrontPage(canvasContext, forceNetwork) + } + + suspend fun getPageDetails(canvasContext: CanvasContext, pageId: String, forceNetwork: Boolean): DataResult { + return dataSource().getPageDetails(canvasContext, pageId, forceNetwork) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsDataSource.kt new file mode 100644 index 0000000000..e1c217aaae --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult + +interface PageDetailsDataSource { + + suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult + + suspend fun getPageDetails(canvasContext: CanvasContext, pageId: String, forceNetwork: Boolean): DataResult +} diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsLocalDataSource.kt new file mode 100644 index 0000000000..9a94fdb0d1 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsLocalDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.PageFacade + +class PageDetailsLocalDataSource( + private val pageFacade: PageFacade +) : PageDetailsDataSource { + + override suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult { + return pageFacade.getFrontPage(canvasContext.id)?.let { DataResult.Success(it) } ?: DataResult.Fail() + } + + override suspend fun getPageDetails(canvasContext: CanvasContext, pageId: String, forceNetwork: Boolean): DataResult { + return pageFacade.getPageDetails(canvasContext.id, pageId)?.let { DataResult.Success(it) } ?: DataResult.Fail() + } +} diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..911fcfb391 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/details/datasource/PageDetailsNetworkDataSource.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details.datasource + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult + +class PageDetailsNetworkDataSource( + private val pageApi: PageAPI.PagesInterface +) : PageDetailsDataSource { + + override suspend fun getFrontPage(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return pageApi.getFrontPage(canvasContext.apiContext(), canvasContext.id, params) + } + + override suspend fun getPageDetails(canvasContext: CanvasContext, pageId: String, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return pageApi.getDetailedPage(canvasContext.apiContext(), canvasContext.id, pageId, params) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PageListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt similarity index 88% rename from apps/student/src/main/java/com/instructure/student/fragment/PageListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt index 4076220db3..811db00ae2 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PageListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.pages.list import android.content.res.Configuration import android.os.Bundle @@ -35,19 +35,26 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.PageListRecyclerAdapter import com.instructure.student.databinding.FragmentCoursePagesBinding import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding import com.instructure.student.events.PageUpdatedEvent +import com.instructure.student.features.pages.details.PageDetailsFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.router.RouteMatcher +import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import javax.inject.Inject @ScreenView(SCREEN_VIEW_PAGE_LIST) @PageView(url = "{canvasContext}/pages") +@AndroidEntryPoint class PageListFragment : ParentFragment(), Bookmarkable { + @Inject + lateinit var repository: PageListRepository + private val binding by viewBinding(FragmentCoursePagesBinding::bind) private lateinit var recyclerBinding: PandaRecyclerRefreshLayoutBinding @@ -97,9 +104,14 @@ class PageListFragment : ParentFragment(), Bookmarkable { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recyclerBinding = PandaRecyclerRefreshLayoutBinding.bind(binding.root) - recyclerAdapter = PageListRecyclerAdapter(requireContext(), canvasContext, object : AdapterToFragmentCallback { + recyclerAdapter = PageListRecyclerAdapter(requireContext(), repository, canvasContext, object : AdapterToFragmentCallback { override fun onRowClicked(page: Page, position: Int, isOpenDetail: Boolean) { - RouteMatcher.route(requireContext(), PageDetailsFragment.makeRoute(canvasContext, page)) + RouteMatcher.route(requireActivity(), + PageDetailsFragment.makeRoute( + canvasContext, + page + ) + ) } override fun onRefreshFinished() { @@ -115,8 +127,11 @@ class PageListFragment : ParentFragment(), Bookmarkable { super.onActivityCreated(savedInstanceState) if (isShowFrontPage) { - val route = PageDetailsFragment.makeRoute(canvasContext, Page.FRONT_PAGE_NAME).apply { ignoreDebounce = true} - RouteMatcher.route(requireContext(), route) + val route = PageDetailsFragment.makeRoute( + canvasContext, + Page.FRONT_PAGE_NAME + ).apply { ignoreDebounce = true} + RouteMatcher.route(requireActivity(), route) } } @@ -176,7 +191,10 @@ class PageListFragment : ParentFragment(), Bookmarkable { companion object { const val SHOW_FRONT_PAGE = "isShowFrontPage" - fun newInstance(route: Route) = if (validRoute(route)) { + fun newInstance(route: Route) = if (validRoute( + route + ) + ) { PageListFragment().apply { arguments = route.arguments diff --git a/apps/student/src/main/java/com/instructure/student/adapter/PageListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt similarity index 86% rename from apps/student/src/main/java/com/instructure/student/adapter/PageListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt index 4ead14350e..12b3ce5d5d 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/PageListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRecyclerAdapter.kt @@ -15,29 +15,29 @@ * */ -package com.instructure.student.adapter +package com.instructure.student.features.pages.list import android.content.Context -import androidx.recyclerview.widget.RecyclerView import android.view.View -import com.instructure.student.R -import com.instructure.student.holders.FrontPageViewHolder -import com.instructure.student.holders.PageViewHolder -import com.instructure.student.interfaces.AdapterToFragmentCallback -import com.instructure.canvasapi2.managers.PageManager +import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.filterWithQuery import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitPaginated import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.pandautils.utils.textAndIconColor import com.instructure.pandautils.utils.toast +import com.instructure.student.R +import com.instructure.student.adapter.BaseListRecyclerAdapter +import com.instructure.student.holders.FrontPageViewHolder +import com.instructure.student.holders.PageViewHolder +import com.instructure.student.interfaces.AdapterToFragmentCallback open class PageListRecyclerAdapter( context: Context, + private val repository: PageListRepository, private val canvasContext: CanvasContext, private val adapterToFragmentCallback: AdapterToFragmentCallback, private var selectedPageTitle: String = FRONT_PAGE_DETERMINER // Page urls only specify the title, not the pageId @@ -61,7 +61,7 @@ open class PageListRecyclerAdapter( } init { - itemCallback = object : BaseListRecyclerAdapter.ItemComparableCallback() { + itemCallback = object : ItemComparableCallback() { override fun compare(page1: Page, page2: Page) = page1.compareTo(page2) override fun areContentsTheSame(item1: Page, item2: Page) = item1.title == item2.title override fun getUniqueItemId(page: Page) = page.id @@ -102,15 +102,7 @@ open class PageListRecyclerAdapter( override fun loadFirstPage() { apiCall = tryWeave { - val refreshing = isRefresh - val newPages = mutableListOf() - awaitPaginated> { - exhaustive = true - onRequestFirst { PageManager.getFirstPagePages(canvasContext, it, refreshing) } - onRequestNext { url, callback -> PageManager.getNextPagePages(url, callback, refreshing) } - onResponse { newPages.addAll(it) } - } - pages = newPages + pages = repository.loadPages(canvasContext, isRefresh) populateData() isAllPagesLoaded = true adapterToFragmentCallback.onRefreshFinished() diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRepository.kt new file mode 100644 index 0000000000..8e7bd18804 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/PageListRepository.kt @@ -0,0 +1,22 @@ +package com.instructure.student.features.pages.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.pages.list.datasource.PageListDataSource +import com.instructure.student.features.pages.list.datasource.PageListLocalDataSource +import com.instructure.student.features.pages.list.datasource.PageListNetworkDataSource + +class PageListRepository( + pageListLocalDataSource: PageListLocalDataSource, + pageListNetworkDataSource: PageListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(pageListLocalDataSource, pageListNetworkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun loadPages(canvasContext: CanvasContext, forceNetwork: Boolean): List { + return dataSource().loadPages(canvasContext, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListDataSource.kt new file mode 100644 index 0000000000..44e026b8bf --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListDataSource.kt @@ -0,0 +1,9 @@ +package com.instructure.student.features.pages.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page + +interface PageListDataSource { + + suspend fun loadPages(canvasContext: CanvasContext, forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListLocalDataSource.kt new file mode 100644 index 0000000000..605ebe5cdb --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListLocalDataSource.kt @@ -0,0 +1,17 @@ +package com.instructure.student.features.pages.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.pandautils.utils.isGroup + +class PageListLocalDataSource( + private val pageFacade: PageFacade +) : PageListDataSource { + + override suspend fun loadPages(canvasContext: CanvasContext, forceNetwork: Boolean): List { + if (canvasContext.isGroup) return emptyList() + + return pageFacade.findByCourseId(canvasContext.id) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListNetworkDataSource.kt new file mode 100644 index 0000000000..04793df0da --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/pages/list/datasource/PageListNetworkDataSource.kt @@ -0,0 +1,18 @@ +package com.instructure.student.features.pages.list.datasource + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.depaginate + +class PageListNetworkDataSource( + private val api: PageAPI.PagesInterface +) : PageListDataSource { + + override suspend fun loadPages(canvasContext: CanvasContext, forceNetwork: Boolean): List { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return api.getFirstPagePages(canvasContext.id, canvasContext.apiContext(), restParams) + .depaginate { api.getNextPagePagesList(it, restParams) }.dataOrThrow + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsDataSource.kt new file mode 100644 index 0000000000..b35badf1d4 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsDataSource.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User + +interface PeopleDetailsDataSource { + suspend fun loadUser(canvasContext: CanvasContext, userId: Long, forceNetwork: Boolean = false): User? + + suspend fun loadMessagePermission(canvasContext: CanvasContext, requestedPermissions: List = emptyList(), user: User?, forceNetwork: Boolean = false): Boolean +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsFragment.kt similarity index 83% rename from apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsFragment.kt index 4d70959b69..f3649cf15f 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PeopleDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsFragment.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.people.details import android.content.res.ColorStateList import android.graphics.Color @@ -23,9 +23,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.GroupManager -import com.instructure.canvasapi2.managers.UserManager +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Recipient @@ -48,8 +46,14 @@ import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.activity.NothingToSeeHereFragment import com.instructure.student.databinding.FragmentPeopleDetailsBinding +import com.instructure.student.features.people.list.PeopleListFragment +import com.instructure.student.fragment.InboxComposeMessageFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +@AndroidEntryPoint @ScreenView(SCREEN_VIEW_PEOPLE_DETAILS) @PageView(url = "{canvasContext}/users/{userId}") class PeopleDetailsFragment : ParentFragment(), Bookmarkable { @@ -60,6 +64,9 @@ class PeopleDetailsFragment : ParentFragment(), Bookmarkable { @PageViewUrlParam(name = "userId") private fun getUserIdForPageView() = userId + @Inject + lateinit var repository: PeopleDetailsRepository + //This is necessary because the groups API doesn't currently support retrieving a single user //from a group, so we have to pass the user in as an argument. private var user by NullableParcelableArg(key = Const.USER) @@ -68,10 +75,6 @@ class PeopleDetailsFragment : ParentFragment(), Bookmarkable { private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private var userCall: WeaveJob? = null - - private var permissionCall: WeaveJob? = null - override fun title(): String = "" override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = layoutInflater.inflate(R.layout.fragment_people_details, container, false) @@ -86,29 +89,23 @@ class PeopleDetailsFragment : ParentFragment(), Bookmarkable { InboxComposeMessageFragment.makeRoute(canvasContext, arrayListOf(Recipient.from(user!!))) } - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } when { - canvasContext.isCourse -> fetchUser() + canvasContext.isCourse && user == null -> fetchUser() user == null -> { //They must have used a deep link, and there's no way to retrieve user data through a //deep link until the groups API gets updated. This redirects the user to the people list. val route = PeopleListFragment.makeRoute(canvasContext) - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } else -> setupUserViews() } } - override fun onDestroy() { - userCall?.cancel() - permissionCall?.cancel() - super.onDestroy() - } - private fun fetchUser() { - userCall = tryWeave { - user = awaitApi { UserManager.getUserForContextId(canvasContext, userId, it, true) } + lifecycleScope.tryLaunch { + user = repository.loadUser(canvasContext, userId, true) setupUserViews() } catch { toast(R.string.errorOccurred) @@ -132,16 +129,8 @@ class PeopleDetailsFragment : ParentFragment(), Bookmarkable { } private fun checkMessagePermission() { - permissionCall = tryWeave { - val id = canvasContext.id - val canMessageUser = when { - canvasContext.isGroup -> GroupManager.getPermissionsAsync(id).awaitOrThrow().send_messages - canvasContext.isCourse -> { - val isTeacher = user?.enrollments?.any { it.courseId == id && (it.isTA || it.isTeacher) } ?: false - isTeacher || CourseManager.getPermissionsAsync(id).awaitOrThrow().send_messages - } - else -> false - } + lifecycleScope.tryLaunch { + val canMessageUser = repository.loadMessagePermission(canvasContext, user, true) binding.compose.setVisible(canMessageUser) } catch { binding.compose.setVisible(false) diff --git a/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsLocalDataSource.kt new file mode 100644 index 0000000000..06df31c2f0 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsLocalDataSource.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.facade.UserFacade + +class PeopleDetailsLocalDataSource(private val userFacade: UserFacade): PeopleDetailsDataSource { + + override suspend fun loadUser(canvasContext: CanvasContext, userId: Long, forceNetwork: Boolean): User? { + return userFacade.getUserById(userId) + } + + override suspend fun loadMessagePermission(canvasContext: CanvasContext, requestedPermissions: List, user: User?, forceNetwork: Boolean): Boolean { + return false + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..05a4125892 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsNetworkDataSource.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.details + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.isCourse +import com.instructure.pandautils.utils.isGroup + +class PeopleDetailsNetworkDataSource( + private val userApi: UserAPI.UsersInterface, + private val courseApi: CourseAPI.CoursesInterface, + private val groupApi: GroupAPI.GroupInterface, +): PeopleDetailsDataSource { + override suspend fun loadUser(canvasContext: CanvasContext, userId: Long, forceNetwork: Boolean): User { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return userApi.getUserForContextId(canvasContext.apiContext(), canvasContext.id, userId, restParams).dataOrThrow + } + + override suspend fun loadMessagePermission(canvasContext: CanvasContext, requestedPermissions: List, user: User?, forceNetwork: Boolean): Boolean { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + val id = canvasContext.id + return when { + canvasContext.isGroup -> groupApi.getGroupPermissions(id, requestedPermissions, restParams).dataOrThrow.send_messages + canvasContext.isCourse -> { + val isTeacher = user?.enrollments?.any { it.courseId == id && (it.isTA || it.isTeacher) } ?: false + isTeacher || courseApi.getCoursePermissions(id, requestedPermissions, restParams).dataOrThrow.send_messages + } + else -> false + } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsRepository.kt new file mode 100644 index 0000000000..4768b8efee --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/details/PeopleDetailsRepository.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class PeopleDetailsRepository( + peopleDetailsNetworkDataSource: PeopleDetailsNetworkDataSource, + peopleDetailsLocalDataSource: PeopleDetailsLocalDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository( + peopleDetailsLocalDataSource, + peopleDetailsNetworkDataSource, + networkStateProvider, + featureFlagProvider +) { + suspend fun loadUser(canvasContext: CanvasContext, userId: Long, forceNetwork: Boolean): User? { + return dataSource().loadUser(canvasContext, userId, forceNetwork) + } + + suspend fun loadMessagePermission(canvasContext: CanvasContext, user: User?, forceNetwork: Boolean): Boolean { + return dataSource().loadMessagePermission( + canvasContext = canvasContext, + user = user, + forceNetwork = forceNetwork, + ) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListDataSource.kt new file mode 100644 index 0000000000..0cba06a946 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult + +interface PeopleListDataSource { + suspend fun loadFirstPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> + suspend fun loadNextPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean, nextUrl: String): DataResult> + suspend fun loadTeachers(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> + suspend fun loadTAs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/PeopleListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListFragment.kt similarity index 85% rename from apps/student/src/main/java/com/instructure/student/fragment/PeopleListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListFragment.kt index 2a5eacf984..b29741cb1d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/PeopleListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListFragment.kt @@ -15,12 +15,13 @@ * */ -package com.instructure.student.fragment +package com.instructure.student.features.people.list import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.pageview.PageView @@ -32,15 +33,22 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.PeopleListRecyclerAdapter import com.instructure.student.databinding.FragmentPeopleListBinding +import com.instructure.student.features.people.details.PeopleDetailsFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.router.RouteMatcher +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @ScreenView(SCREEN_VIEW_PEOPLE_LIST) @PageView(url = "{canvasContext}/users") +@AndroidEntryPoint class PeopleListFragment : ParentFragment(), Bookmarkable { + @Inject + lateinit var repository: PeopleListRepository + private val binding by viewBinding(FragmentPeopleListBinding::bind) private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -50,9 +58,9 @@ class PeopleListFragment : ParentFragment(), Bookmarkable { private var adapterToFragmentCallback = object : AdapterToFragmentCallback { override fun onRowClicked(user: User, position: Int, isOpenDetail: Boolean) { if (canvasContext.isCourse) { - RouteMatcher.route(requireContext(), PeopleDetailsFragment.makeRoute(user.id, canvasContext)) + RouteMatcher.route(requireActivity(), PeopleDetailsFragment.makeRoute(user.id, canvasContext)) } else { - RouteMatcher.route(requireContext(), PeopleDetailsFragment.makeRoute(user, canvasContext)) + RouteMatcher.route(requireActivity(), PeopleDetailsFragment.makeRoute(user, canvasContext)) } } @@ -68,7 +76,7 @@ class PeopleListFragment : ParentFragment(), Bookmarkable { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - recyclerAdapter = PeopleListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback) + recyclerAdapter = PeopleListRecyclerAdapter(requireContext(), lifecycleScope, repository, canvasContext, adapterToFragmentCallback) configureRecyclerView( view, requireContext(), diff --git a/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListLocalDataSource.kt new file mode 100644 index 0000000000..f75c79b4b6 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListLocalDataSource.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.UserFacade + +class PeopleListLocalDataSource(private val userFacade: UserFacade): PeopleListDataSource { + override suspend fun loadFirstPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return DataResult.Success(userFacade.getUsersByCourseId(canvasContext.id)) + } + + override suspend fun loadNextPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean, nextUrl: String): DataResult> { + return DataResult.Success(emptyList()) + } + + override suspend fun loadTeachers(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return DataResult.Success(userFacade.getUsersByCourseIdAndRole(canvasContext.id, Enrollment.EnrollmentType.Teacher)) + } + + override suspend fun loadTAs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return DataResult.Success(userFacade.getUsersByCourseIdAndRole(canvasContext.id, Enrollment.EnrollmentType.Ta)) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListNetworkDataSource.kt new file mode 100644 index 0000000000..c19a59ab42 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListNetworkDataSource.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.list + +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class PeopleListNetworkDataSource( + private val api: UserAPI.UsersInterface +) : PeopleListDataSource { + override suspend fun loadFirstPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return api.getFirstPagePeopleList(canvasContext.id, canvasContext.apiContext(), restParams) + } + + override suspend fun loadNextPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean, nextUrl: String): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return api.getNextPagePeopleList(nextUrl, restParams) + } + + override suspend fun loadTeachers(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return api.getFirstPagePeopleList(canvasContext.id, canvasContext.apiContext(), restParams, UserAPI.EnrollmentType.TEACHER.name.lowercase()) + .depaginate { api.getNextPagePeopleList(it, restParams) } + } + + override suspend fun loadTAs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return api.getFirstPagePeopleList(canvasContext.id, canvasContext.apiContext(), restParams, UserAPI.EnrollmentType.TA.name.lowercase()) + .depaginate { api.getNextPagePeopleList(it, restParams) } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/adapter/PeopleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt similarity index 68% rename from apps/student/src/main/java/com/instructure/student/adapter/PeopleListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt index dfb6f93fdb..a74128b780 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/PeopleListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRecyclerAdapter.kt @@ -14,110 +14,101 @@ * along with this program. If not, see . * */ -package com.instructure.student.adapter +package com.instructure.student.features.people.list import android.content.Context -import androidx.recyclerview.widget.RecyclerView import android.view.View -import com.instructure.canvasapi2.apis.UserAPI -import com.instructure.canvasapi2.managers.UserManager +import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.Enrollment.EnrollmentType import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.NaturalOrderComparator -import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitPaginated import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types -import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.backgroundColor import com.instructure.pandautils.utils.toast import com.instructure.student.R +import com.instructure.student.adapter.ExpandableRecyclerAdapter import com.instructure.student.holders.PeopleHeaderViewHolder import com.instructure.student.holders.PeopleViewHolder import com.instructure.student.interfaces.AdapterToFragmentCallback +import kotlinx.coroutines.CoroutineScope import java.util.Locale class PeopleListRecyclerAdapter( context: Context, - private val mCanvasContext: CanvasContext, - private val mAdapterToFragmentCallback: AdapterToFragmentCallback + private val lifecycleScope: CoroutineScope, + private val repository: PeopleListRepository, + private val canvasContext: CanvasContext, + private val adapterToFragmentCallback: AdapterToFragmentCallback ) : ExpandableRecyclerAdapter(context, EnrollmentType::class.java, User::class.java) { - private val mCourseColor = mCanvasContext.backgroundColor + private val mCourseColor = canvasContext.backgroundColor private val mEnrollmentPriority = mapOf( EnrollmentType.Teacher to 4, EnrollmentType.Ta to 3, EnrollmentType.Student to 2, EnrollmentType.Observer to 1) - private var mApiCalls: WeaveJob? = null init { isExpandedByDefault = true loadData() } - @Suppress("EXPERIMENTAL_FEATURE_WARNING") override fun loadFirstPage() { - mApiCalls = tryWeave { - var canvasContext = mCanvasContext + lifecycleScope.tryLaunch { + var canvasContext = canvasContext // If the canvasContext is a group, and has a course we want to add the Teachers and TAs from that course to the peoples list - if (CanvasContext.Type.isGroup(mCanvasContext) && (mCanvasContext as Group).courseId > 0) { + if (CanvasContext.Type.isGroup(this@PeopleListRecyclerAdapter.canvasContext) && (this@PeopleListRecyclerAdapter.canvasContext as Group).courseId > 0) { // We build a generic CanvasContext with type set to COURSE and give it the CourseId from the group, so that it wil use the course API not the group API - canvasContext = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, mCanvasContext.courseId, "") + canvasContext = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, this@PeopleListRecyclerAdapter.canvasContext.courseId, "") } - // Get Teachers - awaitPaginated> { - onRequestFirst { UserManager.getFirstPagePeopleList(canvasContext, UserAPI.EnrollmentType.TEACHER, isRefresh, it) } - onRequestNext { nextUrl, callback -> UserManager.getNextPagePeopleList(isRefresh, nextUrl, callback) } - onResponse { setNextUrl(""); populateAdapter(it) } - } + val teachers = repository.loadTeachers(canvasContext, isRefresh) + val tas = repository.loadTAs(canvasContext, isRefresh) + val peopleFirstPage = repository.loadFirstPagePeople(canvasContext, isRefresh) + val result = teachers.dataOrThrow + tas.dataOrThrow + peopleFirstPage.dataOrThrow - // Get TAs - awaitPaginated> { - onRequestFirst { UserManager.getFirstPagePeopleList(canvasContext, UserAPI.EnrollmentType.TA, isRefresh, it) } - onRequestNext { nextUrl, callback -> UserManager.getNextPagePeopleList(isRefresh, nextUrl, callback) } - onResponse { setNextUrl(""); populateAdapter(it) } - } + populateAdapter(result) - // Get others - awaitPaginated> { - onRequestFirst { UserManager.getFirstPagePeopleList(mCanvasContext, isRefresh, it) } - onRequestNext { nextUrl, callback -> UserManager.getNextPagePeopleList(isRefresh, nextUrl, callback) } - onResponse { setNextUrl(""); populateAdapter(it) } + if (peopleFirstPage is DataResult.Success>) { + setNextUrl(peopleFirstPage.linkHeaders.nextUrl) } - setNextUrl(null) } catch { context.toast(R.string.errorOccurred) } } override fun loadNextPage(nextURL: String) { - mApiCalls?.next() - } + lifecycleScope.tryLaunch { + val peopleNextPage = repository.loadNextPagePeople(canvasContext, isRefresh, nextURL) - override val isPaginated get() = true + populateAdapter(peopleNextPage.dataOrThrow) - override fun resetData() { - mApiCalls?.cancel() - super.resetData() - } + if (peopleNextPage is DataResult.Success>) { + setNextUrl(peopleNextPage.linkHeaders.nextUrl) + } + } catch { + context.toast(R.string.errorOccurred) + } - override fun cancel() { - mApiCalls?.cancel() } + override val isPaginated get() = true + private fun populateAdapter(result: List) { val (enrolled, unEnrolled) = result.partition { it.enrollments.isNotEmpty() } - enrolled.asSequence().onEach { it.enrollments.sortedByDescending { enrollment-> mEnrollmentPriority[enrollment.type] } } - .groupBy { it.enrollments[0].type } + enrolled + .groupBy { + it.enrollments.sortedByDescending { enrollment -> mEnrollmentPriority[enrollment.type] }[0].type + } .forEach { (type, users) -> addOrUpdateAllItems(type!!, users) } - if (CanvasContext.Type.isGroup(mCanvasContext)) addOrUpdateAllItems(EnrollmentType.NoEnrollment, unEnrolled) + if (CanvasContext.Type.isGroup(canvasContext)) addOrUpdateAllItems(EnrollmentType.NoEnrollment, unEnrolled) notifyDataSetChanged() - mAdapterToFragmentCallback.onRefreshFinished() + adapterToFragmentCallback.onRefreshFinished() } override fun createViewHolder(v: View, viewType: Int): RecyclerView.ViewHolder = @@ -131,7 +122,7 @@ class PeopleListRecyclerAdapter( override fun onBindChildHolder(holder: RecyclerView.ViewHolder, peopleGroupType: EnrollmentType, user: User) { val groupItemCount = getGroupItemCount(peopleGroupType) val itemPosition = storedIndexOfItem(peopleGroupType, user) - (holder as PeopleViewHolder).bind(user, mAdapterToFragmentCallback, mCourseColor, itemPosition == 0, itemPosition == groupItemCount - 1) + (holder as PeopleViewHolder).bind(user, adapterToFragmentCallback, mCourseColor, itemPosition == 0, itemPosition == groupItemCount - 1) } override fun onBindHeaderHolder(holder: RecyclerView.ViewHolder, enrollmentType: EnrollmentType, isExpanded: Boolean) { diff --git a/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRepository.kt new file mode 100644 index 0000000000..c249e2dbe9 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/people/list/PeopleListRepository.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.student.features.people.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class PeopleListRepository( + peopleListLocalDataSource: PeopleListLocalDataSource, + peopleListNetworkDataSource: PeopleListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(peopleListLocalDataSource, peopleListNetworkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun loadFirstPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return dataSource().loadFirstPagePeople(canvasContext, forceNetwork) + } + + suspend fun loadNextPagePeople(canvasContext: CanvasContext, forceNetwork: Boolean, nextUrl: String = ""): DataResult> { + return dataSource().loadNextPagePeople(canvasContext, forceNetwork, nextUrl) + } + + suspend fun loadTeachers(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return dataSource().loadTeachers(canvasContext, forceNetwork) + } + + suspend fun loadTAs(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return dataSource().loadTAs(canvasContext, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListDataSource.kt new file mode 100644 index 0000000000..b3604b313c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.quiz.list + +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz + +interface QuizListDataSource { + + suspend fun loadQuizzes(contextType: String, contextId: Long, forceNetwork: Boolean): List + + suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/QuizListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt similarity index 79% rename from apps/student/src/main/java/com/instructure/student/fragment/QuizListFragment.kt rename to apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt index a740d0c43b..14a8b3b566 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/QuizListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListFragment.kt @@ -1,27 +1,29 @@ /* - * Copyright (C) 2016 - present Instructure, Inc. + * Copyright (C) 2023 - present Instructure, Inc. * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . * */ -package com.instructure.student.fragment +package com.instructure.student.features.quiz.list import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.utils.ApiPrefs @@ -34,17 +36,25 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.QuizListRecyclerAdapter import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding import com.instructure.student.databinding.QuizListLayoutBinding -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.assignments.list.AssignmentListFragment +import com.instructure.student.fragment.BasicQuizViewFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.router.RouteMatcher +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @ScreenView(SCREEN_VIEW_QUIZ_LIST) @PageView(url = "{canvasContext}/quizzes") +@AndroidEntryPoint class QuizListFragment : ParentFragment(), Bookmarkable { + @Inject + lateinit var quizListRepository: QuizListRepository + private val binding by viewBinding(QuizListLayoutBinding::bind) private lateinit var recyclerBinding: PandaRecyclerRefreshLayoutBinding @@ -69,7 +79,7 @@ class QuizListFragment : ParentFragment(), Bookmarkable { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { recyclerBinding = PandaRecyclerRefreshLayoutBinding.bind(binding.root) - recyclerAdapter = QuizListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback) + recyclerAdapter = QuizListRecyclerAdapter(requireContext(), canvasContext, adapterToFragmentCallback, quizListRepository, lifecycleScope) configureRecyclerView( view, requireContext(), @@ -137,6 +147,7 @@ class QuizListFragment : ParentFragment(), Bookmarkable { override val bookmark: Bookmarker get() = Bookmarker(true, canvasContext) + @androidx.annotation.OptIn(com.google.android.material.badge.ExperimentalBadgeUtils::class) private fun rowClick(quiz: Quiz) { val navigation = navigation if (navigation != null) { @@ -149,9 +160,9 @@ class QuizListFragment : ParentFragment(), Bookmarkable { AssignmentListFragment::class.java -> AssignmentDetailsFragment::class.java else -> null } - RouteMatcher.routeUrl(requireContext(), quiz.htmlUrl!!, ApiPrefs.domain, secondaryClass = secondaryClass) + RouteMatcher.routeUrl(requireActivity(), quiz.htmlUrl!!, ApiPrefs.domain, secondaryClass = secondaryClass) } else { - RouteMatcher.route(requireContext(), BasicQuizViewFragment.makeRoute(canvasContext, quiz, quiz.url!!)) + RouteMatcher.route(requireActivity(), BasicQuizViewFragment.makeRoute(canvasContext, quiz, quiz.url!!)) } } } diff --git a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListLocalDataSource.kt new file mode 100644 index 0000000000..2d12e1d038 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListLocalDataSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.quiz.list + +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.QuizDao + +class QuizListLocalDataSource( + private val quizDao: QuizDao, + private val courseSettingsDao: CourseSettingsDao +) : QuizListDataSource { + override suspend fun loadQuizzes(contextType: String, contextId: Long, forceNetwork: Boolean): List { + return quizDao.findByCourseId(contextId).map { it.toApiModel() } + } + + override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return courseSettingsDao.findByCourseId(courseId)?.toApiModel() + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListNetworkDataSource.kt new file mode 100644 index 0000000000..563d087ad8 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListNetworkDataSource.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.quiz.list + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.depaginate + +class QuizListNetworkDataSource(private val quizApi: QuizAPI.QuizInterface, private val courseApi: CourseAPI.CoursesInterface) : QuizListDataSource { + + override suspend fun loadQuizzes(contextType: String, contextId: Long, forceNetwork: Boolean): List { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return quizApi.getFirstPageQuizzesList(contextType, contextId, restParams).depaginate { + quizApi.getNextPageQuizzesList(it, restParams) + }.dataOrThrow + } + + override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return courseApi.getCourseSettings(courseId, restParams).dataOrNull + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/adapter/QuizListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRecyclerAdapter.kt similarity index 74% rename from apps/student/src/main/java/com/instructure/student/adapter/QuizListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRecyclerAdapter.kt index 02cf7b554c..c65219affd 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/QuizListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRecyclerAdapter.kt @@ -1,36 +1,34 @@ /* - * Copyright (C) 2016 - present Instructure, Inc. + * Copyright (C) 2023 - present Instructure, Inc. * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . * */ -package com.instructure.student.adapter +package com.instructure.student.features.quiz.list import android.content.Context import android.view.View +import androidx.lifecycle.LifecycleCoroutineScope import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.QuizManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.CourseSettings import com.instructure.canvasapi2.models.Quiz import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.filterWithQuery -import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitPaginated import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.pandarecycler.interfaces.ViewHolderHeaderClicked import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types @@ -38,20 +36,21 @@ import com.instructure.pandautils.utils.orDefault import com.instructure.pandautils.utils.textAndIconColor import com.instructure.pandautils.utils.toast import com.instructure.student.R +import com.instructure.student.adapter.ExpandableRecyclerAdapter import com.instructure.student.holders.ExpandableViewHolder import com.instructure.student.holders.QuizViewHolder import com.instructure.student.interfaces.AdapterToFragmentCallback class QuizListRecyclerAdapter( - context: Context, - private val canvasContext: CanvasContext, - private val adapterToFragmentCallback: AdapterToFragmentCallback? + context: Context, + private val canvasContext: CanvasContext, + private val adapterToFragmentCallback: AdapterToFragmentCallback?, + private val repository: QuizListRepository, + private val lifecycleScope: LifecycleCoroutineScope, ) : ExpandableRecyclerAdapter(context, String::class.java, Quiz::class.java) { private var quizzes = emptyList() - private var apiCall: WeaveJob? = null - private var settings: CourseSettings? = null var searchQuery = "" @@ -96,20 +95,12 @@ class QuizListRecyclerAdapter( } override fun loadFirstPage() { - apiCall = tryWeave { - val refreshing = isRefresh - val newQuizzes = mutableListOf() - settings = CourseManager.getCourseSettingsAsync(canvasContext.id, refreshing).await().dataOrNull - awaitPaginated { - exhaustive = true - onRequestFirst { QuizManager.getFirstPageQuizList(canvasContext, refreshing, it) } - onRequestNext { url, callback -> QuizManager.getNextPageQuizList(url, refreshing, callback) } - onResponse { newQuizzes.addAll(it) } - } - isAllPagesLoaded = true - quizzes = newQuizzes + lifecycleScope.tryLaunch { + settings = repository.loadCourseSettings(canvasContext.id, isRefresh) + quizzes = repository.loadQuizzes(canvasContext.type.apiString, canvasContext.id, isRefresh) populateData() onCallbackFinished(ApiType.API) + isAllPagesLoaded = true } catch { context.toast(R.string.errorOccurred) } @@ -157,8 +148,4 @@ class QuizListRecyclerAdapter( } } - override fun cancel() { - apiCall?.cancel() - } - } diff --git a/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRepository.kt b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRepository.kt new file mode 100644 index 0000000000..19d4416ce5 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/features/quiz/list/QuizListRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.quiz.list + +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +class QuizListRepository( + localDataSource: QuizListLocalDataSource, + networkDataSource: QuizListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun loadQuizzes(contextType: String, contextId: Long, forceNetwork: Boolean): List { + return dataSource().loadQuizzes(contextType, contextId, forceNetwork) + } + + suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return dataSource().loadCourseSettings(courseId, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AnnouncementListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AnnouncementListFragment.kt index fcefffad62..a214efba36 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AnnouncementListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AnnouncementListFragment.kt @@ -24,6 +24,7 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_ANNOUNCEMENT_LIST import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.makeBundle +import com.instructure.student.features.discussion.list.DiscussionListFragment @ScreenView(SCREEN_VIEW_ANNOUNCEMENT_LIST) @PageView(url = "{canvasContext}/announcements") diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt index 82129d4483..594680ba04 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ApplicationSettingsFragment.kt @@ -25,6 +25,7 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.loginapi.login.dialog.NoInternetConnectionDialog @@ -34,7 +35,9 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.about.AboutFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment +import com.instructure.pandautils.features.offline.sync.settings.SyncSettingsFragment import com.instructure.pandautils.fragments.RemoteConfigParamsFragment +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade import com.instructure.pandautils.utils.* import com.instructure.student.BuildConfig import com.instructure.student.R @@ -43,22 +46,47 @@ import com.instructure.student.activity.SettingsActivity import com.instructure.student.databinding.FragmentApplicationSettingsBinding import com.instructure.student.dialog.LegalDialogStyled import com.instructure.student.mobius.settings.pairobserver.ui.PairObserverFragment +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import javax.inject.Inject @ScreenView(SCREEN_VIEW_APPLICATION_SETTINGS) @PageView(url = "profile/settings") +@AndroidEntryPoint class ApplicationSettingsFragment : ParentFragment() { + @Inject + lateinit var syncSettingsFacade: SyncSettingsFacade + + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + + @Inject + lateinit var networkStateProvider: NetworkStateProvider + private val binding by viewBinding(FragmentApplicationSettingsBinding::bind) override fun title(): String = getString(R.string.settings) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = - inflater.inflate(R.layout.fragment_application_settings, container, false) + inflater.inflate(R.layout.fragment_application_settings, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) applyTheme() setupViews() + + networkStateProvider.isOnlineLiveData.observe(this) { isOnline -> + handleOnlineState(isOnline == true) + } + } + + private fun handleOnlineState(online: Boolean) = with(binding) { + profileSettings.alpha = if (online) 1f else 0.5f + pushNotifications.alpha = if (online) 1f else 0.5f + emailNotifications.alpha = if (online) 1f else 0.5f + pairObserver.alpha = if (online) 1f else 0.5f + legal.alpha = if (online) 1f else 0.5f } override fun applyTheme() = with(binding) { @@ -68,7 +96,7 @@ class ApplicationSettingsFragment : ParentFragment() { @SuppressLint("SetTextI18n") private fun setupViews() = with(binding) { - profileSettings.onClick { + profileSettings.onClickWithRequireNetwork { val frag = if (ApiPrefs.isStudentView) { // Profile settings not available in Student View NothingToSeeHereFragment.newInstance() @@ -85,25 +113,21 @@ class ApplicationSettingsFragment : ParentFragment() { accountPreferences.onClick { addFragment(AccountPreferencesFragment.newInstance()) } } - legal.onClick { LegalDialogStyled().show(requireFragmentManager(), LegalDialogStyled.TAG) } + legal.onClickWithRequireNetwork { LegalDialogStyled().show(requireFragmentManager(), LegalDialogStyled.TAG) } pinAndFingerprint.setGone() // TODO: Wire up once implemented if (ApiPrefs.canGeneratePairingCode == true) { pairObserver.setVisible() - pairObserver.onClick { - if (APIHelper.hasNetworkConnection()) { - addFragment(PairObserverFragment.newInstance()) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } + pairObserver.onClickWithRequireNetwork { + addFragment(PairObserverFragment.newInstance()) } } - pushNotifications.onClick { + pushNotifications.onClickWithRequireNetwork { addFragment(PushNotificationPreferencesFragment.newInstance()) } - emailNotifications.onClick { + emailNotifications.onClickWithRequireNetwork { addFragment(EmailNotificationPreferencesFragment.newInstance()) } @@ -111,6 +135,8 @@ class ApplicationSettingsFragment : ParentFragment() { AboutFragment.newInstance().show(childFragmentManager, null) } + setUpSyncSettings() + if (ApiPrefs.canvasForElementary) { elementaryViewSwitch.isChecked = ApiPrefs.elementaryDashboardEnabledOverride elementaryViewLayout.setVisible() @@ -154,11 +180,37 @@ class ApplicationSettingsFragment : ParentFragment() { } } + private fun setUpSyncSettings() { + lifecycleScope.launch { + if (!featureFlagProvider.offlineEnabled()) { + binding.offlineContentDivider.setGone() + binding.offlineContentTitle.setGone() + binding.offlineSyncSettingsContainer.setGone() + } else { + syncSettingsFacade.getSyncSettingsListenable().observe(viewLifecycleOwner) { syncSettings -> + if (syncSettings == null) { + binding.offlineSyncSettingsContainer.setGone() + } else { + binding.offlineSyncSettingsStatus.text = if (syncSettings.autoSyncEnabled) { + getString(syncSettings.syncFrequency.readable) + } else { + getString(R.string.syncSettings_manualDescription) + } + } + } + + binding.offlineSyncSettingsContainer.onClick { + addFragment(SyncSettingsFragment.newInstance()) + } + } + } + } + private fun setUpSubscribeToCalendarFeed() { val calendarFeed = ApiPrefs.user?.calendar?.ics if (!calendarFeed.isNullOrEmpty()) { binding.subscribeToCalendar.apply { - setVisible() + setVisible() onClick { AlertDialog.Builder(requireContext()) .setMessage(R.string.subscribeToCalendarMessage) @@ -166,7 +218,7 @@ class ApplicationSettingsFragment : ParentFragment() { dialog.dismiss() openCalendarLink(calendarFeed) } - .setNegativeButton(R.string.cancel, {dialog, _ -> dialog.dismiss()}) + .setNegativeButton(R.string.cancel, { dialog, _ -> dialog.dismiss() }) .showThemed() } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt index 3e0708c030..4d3093f698 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentBasicFragment.kt @@ -142,7 +142,7 @@ class AssignmentBasicFragment : ParentFragment() { loadHtmlJob = assignmentWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), description, { assignmentWebViewWrapper.loadHtml(it, assignment.name) }, { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) }) } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt index aa6fdfcc0c..17834112be 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CalendarEventFragment.kt @@ -228,7 +228,7 @@ class CalendarEventFragment : ParentFragment() { loadHtmlJob = calendarEventWebViewWrapper?.webView?.loadHtmlWithIframes(requireContext(), content, { html -> loadCalendarHtml(html, it.title) }) { url -> - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, url) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, url) } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt index 41f45cdc9f..0c3c583351 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CalendarFragment.kt @@ -35,7 +35,7 @@ import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.ViewStyler import com.instructure.student.R import com.instructure.student.activity.NavigationActivity -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.router.RouteMatcher import io.flutter.plugin.common.MethodCall @@ -107,7 +107,7 @@ class CalendarFragment : ParentFragment() { } } - route?.let { RouteMatcher.route(requireContext(), it) } + route?.let { RouteMatcher.route(requireActivity(), it) } } private fun showDialog(call: MethodCall, result: MethodChannel.Result) { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt index 1008ac4e19..2bfd905eaf 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/DashboardFragment.kt @@ -27,6 +27,7 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.GridLayoutManager @@ -45,6 +46,7 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment import com.instructure.pandautils.features.dashboard.notifications.DashboardNotificationsFragment +import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.DashboardRecyclerAdapter @@ -56,19 +58,34 @@ import com.instructure.student.dialog.EditCourseNicknameDialog import com.instructure.student.events.CoreDataFinishedLoading import com.instructure.student.events.CourseColorOverlayToggledEvent import com.instructure.student.events.ShowGradesToggledEvent +import com.instructure.student.features.coursebrowser.CourseBrowserFragment +import com.instructure.student.features.dashboard.DashboardRepository import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.interfaces.CourseAdapterToFragmentCallback import com.instructure.student.router.RouteMatcher import com.instructure.student.util.StudentPrefs +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import javax.inject.Inject private const val LIST_SPAN_COUNT = 1 @ScreenView(SCREEN_VIEW_DASHBOARD) @PageView +@AndroidEntryPoint class DashboardFragment : ParentFragment() { + @Inject + lateinit var repository: DashboardRepository + + @Inject + lateinit var featureFlagProvider: FeatureFlagProvider + + @Inject + lateinit var networkStateProvider: NetworkStateProvider + private val binding by viewBinding(FragmentCourseGridBinding::bind) private lateinit var recyclerBinding: CourseGridRecyclerRefreshLayoutBinding @@ -96,7 +113,13 @@ class DashboardFragment : ParentFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recyclerBinding = CourseGridRecyclerRefreshLayoutBinding.bind(binding.root) + applyTheme() + + networkStateProvider.isOnlineLiveData.observe(this) { online -> + recyclerAdapter?.refresh() + if (online) recyclerBinding.swipeRefreshLayout.isRefreshing = true + } } @@ -111,17 +134,17 @@ class DashboardFragment : ParentFragment() { } override fun onSeeAllCourses() { - RouteMatcher.route(requireContext(), EditDashboardFragment.makeRoute()) + RouteMatcher.route(requireActivity(), EditDashboardFragment.makeRoute()) } override fun onGroupSelected(group: Group) { canvasContext = group - RouteMatcher.route(requireContext(), CourseBrowserFragment.makeRoute(group)) + RouteMatcher.route(requireActivity(), CourseBrowserFragment.makeRoute(group)) } override fun onCourseSelected(course: Course) { canvasContext = course - RouteMatcher.route(requireContext(), CourseBrowserFragment.makeRoute(course)) + RouteMatcher.route(requireActivity(), CourseBrowserFragment.makeRoute(course)) } @Suppress("EXPERIMENTAL_FEATURE_WARNING") @@ -156,10 +179,15 @@ class DashboardFragment : ParentFragment() { } }.show(requireFragmentManager(), ColorPickerDialog::class.java.simpleName) } - }) + + override fun onManageOfflineContent(course: Course) { + RouteMatcher.route(requireActivity(), OfflineContentFragment.makeRoute(course)) + } + }, repository) configureRecyclerView() recyclerBinding.listView.isSelectionEnabled = false + initMenu() } override fun applyTheme() { @@ -168,22 +196,32 @@ class DashboardFragment : ParentFragment() { // Styling done in attachNavigationDrawer navigation?.attachNavigationDrawer(this@DashboardFragment, toolbar) - toolbar.setMenu(R.menu.menu_dashboard) { item -> - when (item.itemId) { - R.id.menu_dashboard_cards -> changeDashboardLayout(item) + recyclerAdapter?.notifyDataSetChanged() + } + } + + private fun initMenu() = with(binding) { + toolbar.setMenu(R.menu.menu_dashboard) { item -> + when (item.itemId) { + R.id.menu_dashboard_cards -> changeDashboardLayout(item) + R.id.menu_dashboard_offline -> activity?.withRequireNetwork { + RouteMatcher.route(requireActivity(), OfflineContentFragment.makeRoute()) } } + } - val dashboardLayoutMenuItem = toolbar.menu.findItem(R.id.menu_dashboard_cards) - val menuIconRes = - if (StudentPrefs.listDashboard) R.drawable.ic_grid_dashboard else R.drawable.ic_list_dashboard - dashboardLayoutMenuItem.setIcon(menuIconRes) + val dashboardLayoutMenuItem = toolbar.menu.findItem(R.id.menu_dashboard_cards) + val menuIconRes = if (StudentPrefs.listDashboard) R.drawable.ic_grid_dashboard else R.drawable.ic_list_dashboard + dashboardLayoutMenuItem.setIcon(menuIconRes) - val menuTitleRes = - if (StudentPrefs.listDashboard) R.string.dashboardSwitchToGridView else R.string.dashboardSwitchToListView - dashboardLayoutMenuItem.setTitle(menuTitleRes) + val menuTitleRes = if (StudentPrefs.listDashboard) R.string.dashboardSwitchToGridView else R.string.dashboardSwitchToListView + dashboardLayoutMenuItem.setTitle(menuTitleRes) - recyclerAdapter?.notifyDataSetChanged() + lifecycleScope.launch { + if (!featureFlagProvider.offlineEnabled()) { + toolbar.menu.removeItem(R.id.menu_dashboard_offline) + toolbar.menu.findItem(R.id.menu_dashboard_cards).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } } } @@ -272,7 +310,7 @@ class DashboardFragment : ParentFragment() { if (!APIHelper.hasNetworkConnection()) { toast(R.string.notAvailableOffline) } else { - RouteMatcher.route(requireContext(), EditDashboardFragment.makeRoute()) + RouteMatcher.route(requireActivity(), EditDashboardFragment.makeRoute()) } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt index b4c6148bc8..55e375915c 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxComposeMessageFragment.kt @@ -41,7 +41,7 @@ import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment import com.instructure.pandautils.features.file.upload.FileUploadDialogParent -import com.instructure.pandautils.room.common.daos.AttachmentDao +import com.instructure.pandautils.room.appdatabase.daos.AttachmentDao import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.CanvasContextSpinnerAdapter @@ -216,7 +216,7 @@ class InboxComposeMessageFragment : ParentFragment(), FileUploadDialogParent { } else { val canvasContext = selectedContext ?: CanvasContext.fromContextCode(conversation?.contextCode) ?: return@onClick - RouteMatcher.route(requireContext(), InboxRecipientsFragment.makeRoute(canvasContext, chips.recipients)) + RouteMatcher.route(requireActivity(), InboxRecipientsFragment.makeRoute(canvasContext, chips.recipients)) } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt index 826f7f7095..adb03b759b 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InboxConversationFragment.kt @@ -355,7 +355,7 @@ class InboxConversationFragment : ParentFragment() { users.map { Recipient.from(it) }, longArrayOf(), null) - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } // Same as reply all but scoped to a message @@ -366,7 +366,7 @@ class InboxConversationFragment : ParentFragment() { getMessageRecipientsForReplyAll(message).map { Recipient.from(it) }, longArrayOf(), message) - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } private fun addMessage(message: Message, isReply: Boolean) { @@ -376,7 +376,7 @@ class InboxConversationFragment : ParentFragment() { getMessageRecipientsForReply(message).map { Recipient.from(it) }, adapter.getMessageChainIdsForMessage(message), message) - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } private fun getMessageRecipientsForReplyAll(message: Message): List { diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt index f7540bba2c..0675d6fa6d 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt @@ -19,7 +19,6 @@ package com.instructure.student.fragment -import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle @@ -29,6 +28,7 @@ import android.view.View import android.view.ViewGroup import android.webkit.WebView import android.widget.ProgressBar +import androidx.fragment.app.FragmentActivity import androidx.work.WorkManager import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.canvasapi2.managers.SubmissionManager @@ -46,6 +46,7 @@ import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.R import com.instructure.student.databinding.FragmentWebviewBinding +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment import com.instructure.student.router.RouteMatcher import kotlinx.coroutines.Job import org.greenrobot.eventbus.EventBus @@ -597,12 +598,12 @@ open class InternalWebviewFragment : ParentFragment() { fun makeRouteHTML(canvasContext: CanvasContext, html: String): Route = makeRoute(canvasContext, null, null, false, html) - fun loadInternalWebView(context: Context?, route: Route) { - if (context == null) { + fun loadInternalWebView(activity: FragmentActivity?, route: Route) { + if (activity == null) { Logger.e("loadInternalWebView could not complete, context is null") return } - RouteMatcher.route(context, route) + RouteMatcher.route(activity, route) } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt index 3100f10a62..91dc14a6f4 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/LtiLaunchFragment.kt @@ -17,7 +17,6 @@ package com.instructure.student.fragment -import android.content.Context import android.net.Uri import android.os.Bundle import android.view.LayoutInflater @@ -25,6 +24,7 @@ import android.view.View import android.view.ViewGroup import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.managers.AssignmentManager import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.* @@ -230,9 +230,9 @@ class LtiLaunchFragment : ParentFragment() { return LtiLaunchFragment().withArgs(route.argsWithContext) } - fun routeLtiLaunchFragment(context: Context, canvasContext: CanvasContext?, url: String) { - val args = makeLTIBundle(URLDecoder.decode(url, "utf-8"), context.getString(R.string.utils_externalToolTitle), true) - RouteMatcher.route(context, Route(LtiLaunchFragment::class.java, canvasContext, args)) + fun routeLtiLaunchFragment(activity: FragmentActivity, canvasContext: CanvasContext?, url: String) { + val args = makeLTIBundle(URLDecoder.decode(url, "utf-8"), activity.getString(R.string.utils_externalToolTitle), true) + RouteMatcher.route(activity, Route(LtiLaunchFragment::class.java, canvasContext, args)) } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/MasteryPathOptionsFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/MasteryPathOptionsFragment.kt index bbfbf9a70f..49a23ad1cb 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/MasteryPathOptionsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/MasteryPathOptionsFragment.kt @@ -34,6 +34,7 @@ import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.MasteryPathOptionsRecyclerAdapter import com.instructure.student.databinding.FragmentMasteryPathsOptionsBinding +import com.instructure.student.features.modules.list.ModuleListFragment import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.router.RouteMatcher diff --git a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt index 933b3704f6..cdb1024802 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/NotificationListFragment.kt @@ -17,13 +17,13 @@ package com.instructure.student.fragment -import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.StreamItem.Type.* @@ -43,9 +43,9 @@ import com.instructure.student.activity.ParentActivity import com.instructure.student.adapter.NotificationListRecyclerAdapter import com.instructure.student.databinding.FragmentListNotificationBinding import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.interfaces.NotificationAdapterToFragmentCallback -import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment +import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.router.RouteMatcher @ScreenView(SCREEN_VIEW_NOTIFICATION_LIST) @@ -221,9 +221,10 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager } companion object { - fun addFragmentForStreamItem(streamItem: StreamItem, context: Context, fromWidget: Boolean) { + fun addFragmentForStreamItem(streamItem: StreamItem, activity: FragmentActivity, fromWidget: Boolean) { if (fromWidget) { - RouteMatcher.routeUrl(context, streamItem.url ?: streamItem.htmlUrl) // If we get null URLs, we can't route, so the behavior will just launch the app to whatever screen they were on last + RouteMatcher.routeUrl(activity, streamItem.url ?: streamItem.htmlUrl) + // If we get null URLs, we can't route, so the behavior will just launch the app to whatever screen they were on last return } @@ -232,9 +233,9 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager if (conversation != null) { // Check to see if the conversation has been deleted. if (conversation.isDeleted) { - Toast.makeText(context, R.string.deleteConversation, Toast.LENGTH_SHORT).show() + Toast.makeText(activity, R.string.deleteConversation, Toast.LENGTH_SHORT).show() } else { - RouteMatcher.route(context, InboxConversationFragment.makeRoute(conversation, null)) + RouteMatcher.route(activity, InboxConversationFragment.makeRoute(conversation, null)) } } return @@ -247,17 +248,17 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager if (canvasContext !is Course) return if (streamItem.assignment == null) { - RouteMatcher.route(context, AssignmentDetailsFragment.makeRoute(canvasContext, streamItem.assignmentId)) + RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, streamItem.assignmentId)) } else { // Add an empty submission with the grade to the assignment so that we can see the score. streamItem.assignment?.submission = Submission(grade = streamItem.grade) - RouteMatcher.route(context, AssignmentDetailsFragment.makeRoute(canvasContext, streamItem.assignment!!.id)) + RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, streamItem.assignment!!.id)) } null } ANNOUNCEMENT, DISCUSSION_TOPIC -> { val route = DiscussionRouterFragment.makeRoute(canvasContext, streamItem.discussionTopicId) - RouteMatcher.route(context, route) + RouteMatcher.route(activity, route) null } MESSAGE -> { @@ -268,11 +269,11 @@ class NotificationListFragment : ParentFragment(), Bookmarkable, FragmentManager } } COLLABORATION -> UnsupportedTabFragment.makeRoute(canvasContext, Tab.COLLABORATIONS_ID) - CONFERENCE -> ConferenceListFragment.makeRoute(canvasContext) + CONFERENCE -> ConferenceListRepositoryFragment.makeRoute(canvasContext) else -> UnsupportedFeatureFragment.makeRoute(canvasContext, featureName = streamItem.type, url = streamItem.url ?: streamItem.htmlUrl) } - if (route != null) RouteMatcher.route(context, route) + if (route != null) RouteMatcher.route(activity, route) } fun makeRoute(canvasContext: CanvasContext): Route = Route(NotificationListFragment::class.java, canvasContext, Bundle()) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt index f4939a341f..2f52aadbb7 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ParentFragment.kt @@ -379,6 +379,14 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati } } + fun openLocalMedia(mime: String?, path: String?, filename: String?, canvasContext: CanvasContext, useOutsideApps: Boolean = false) { + val owner = activity ?: return + onMainThread { + openMediaBundle = OpenMediaAsyncTaskLoader.createLocalBundle(canvasContext, mime, path, filename, useOutsideApps) + LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(owner), openMediaBundle, loaderCallbacks, R.id.openMediaLoaderID) + } + } + fun openMedia(canvasContext: CanvasContext, url: String, filename: String?) { val owner = activity ?: return onMainThread { @@ -442,9 +450,11 @@ abstract class ParentFragment : DialogFragment(), FragmentInteractions, Navigati override fun getFragment(): Fragment? = this fun setEmptyView(emptyView: EmptyView, drawableId: Int, titleId: Int, messageId: Int) { - emptyView.setEmptyViewImage(requireContext().getDrawableCompat(drawableId)) - emptyView.setTitleText(titleId) - emptyView.setMessageText(messageId) - emptyView.setListEmpty() + if (context != null) { + emptyView.setEmptyViewImage(requireContext().getDrawableCompat(drawableId)) + emptyView.setTitleText(titleId) + emptyView.setMessageText(messageId) + emptyView.setListEmpty() + } } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt index 1db8756f7d..dc9b95e04f 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ToDoListFragment.kt @@ -38,7 +38,7 @@ import com.instructure.student.R import com.instructure.student.adapter.TodoListRecyclerAdapter import com.instructure.student.databinding.FragmentListTodoBinding import com.instructure.student.databinding.PandaRecyclerRefreshLayoutBinding -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.interfaces.NotificationAdapterToFragmentCallback import com.instructure.student.router.RouteMatcher @@ -166,19 +166,19 @@ class ToDoListFragment : ParentFragment() { if (toDo.assignment!!.discussionTopicHeader != null) { val groupTopic = toDo.assignment!!.discussionTopicHeader!!.groupTopicChildren.firstOrNull() if (groupTopic == null) { // Launch discussion details fragment - RouteMatcher.route(requireContext(), DiscussionRouterFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.discussionTopicHeader!!)) + RouteMatcher.route(requireActivity(), DiscussionRouterFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.discussionTopicHeader!!)) } else { // Launch discussion details fragment with the group - RouteMatcher.route(requireContext(), DiscussionRouterFragment.makeRoute(CanvasContext.emptyGroupContext(groupTopic.groupId), groupTopic.id)) + RouteMatcher.route(requireActivity(), DiscussionRouterFragment.makeRoute(CanvasContext.emptyGroupContext(groupTopic.groupId), groupTopic.id)) } } else { // Launch assignment details fragment. - RouteMatcher.route(requireContext(), AssignmentDetailsFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.id)) + RouteMatcher.route(requireActivity(), AssignmentDetailsFragment.makeRoute(toDo.canvasContext!!, toDo.assignment!!.id)) } } toDo?.scheduleItem != null -> // It's a Calendar event from the Upcoming API. - RouteMatcher.route(requireContext(), CalendarEventFragment.makeRoute(toDo.canvasContext!!, toDo.scheduleItem!!)) + RouteMatcher.route(requireActivity(), CalendarEventFragment.makeRoute(toDo.canvasContext!!, toDo.scheduleItem!!)) toDo?.quiz != null -> // It's a Quiz let's launch the quiz details fragment - RouteMatcher.route(requireContext(), BasicQuizViewFragment.makeRoute(toDo.canvasContext!!, toDo.quiz!!, toDo.quiz!!.url!!)) + RouteMatcher.route(requireActivity(), BasicQuizViewFragment.makeRoute(toDo.canvasContext!!, toDo.quiz!!, toDo.quiz!!.url!!)) } } diff --git a/apps/student/src/main/java/com/instructure/student/holders/CourseViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/CourseViewHolder.kt index 5f383adb06..8a4c413fec 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/CourseViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/CourseViewHolder.kt @@ -28,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseGrade import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.pandautils.features.dashboard.DashboardCourseItem import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.databinding.ViewholderCourseCardBinding @@ -41,7 +42,9 @@ class CourseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { } @SuppressLint("SetTextI18n") - fun bind(course: Course, callback: CourseAdapterToFragmentCallback): Unit = with(ViewholderCourseCardBinding.bind(itemView)) { + fun bind(courseItem: DashboardCourseItem, isOfflineEnabled: Boolean, callback: CourseAdapterToFragmentCallback): Unit = with(ViewholderCourseCardBinding.bind(itemView)) { + val course = courseItem.course + titleTextView.text = course.name courseCode.text = course.courseCode @@ -56,6 +59,18 @@ class CourseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { courseColorIndicator.backgroundTintList = ColorStateList.valueOf(course.backgroundColor) courseColorIndicator.setVisible(StudentPrefs.hideCourseColorOverlay) + if (courseItem.available || !isOfflineEnabled) { + cardView.alpha = 1f + cardView.isEnabled = true + overflow.isEnabled = true + } else { + cardView.alpha = 0.5f + cardView.isEnabled = false + overflow.isEnabled = false + } + + offlineSyncIcon.setVisible(courseItem.availableOffline) + cardView.setOnClickListener { callback.onCourseSelected(course)} overflow.onClickWithRequireNetwork { @@ -65,12 +80,16 @@ class CourseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // Add things to the popup menu menu.add(0, 0, 0, R.string.editNickname) menu.add(0, 1, 1, R.string.editCourseColor) + if (isOfflineEnabled) { + menu.add(0, 2, 2, R.string.course_menu_manage_offline_content) + } // Add click listener popup.setOnMenuItemClickListener { item -> when (item.itemId) { 0 -> callback.onEditCourseNickname(course) 1 -> callback.onPickCourseColor(course) + 2 -> callback.onManageOfflineContent(course) } true } @@ -115,7 +134,7 @@ class CourseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { } } else { val scoreString = NumberHelper.doubleToPercentage(courseGrade.currentScore, 2) - textView.text = "${if(courseGrade.hasCurrentGradeString()) courseGrade.currentGrade else ""} $scoreString" + textView.text = if(courseGrade.hasCurrentGradeString()) "${courseGrade.currentGrade} $scoreString" else scoreString textView.contentDescription = getContentDescriptionForMinusGradeString(courseGrade.currentGrade ?: "", context) } } diff --git a/apps/student/src/main/java/com/instructure/student/holders/FileViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/FileViewHolder.kt index 87092ab22a..db3ca8f9f4 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/FileViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/FileViewHolder.kt @@ -26,9 +26,9 @@ import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.canvasapi2.utils.isValid import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.FileFolderCallback +import com.instructure.student.features.files.list.FileFolderCallback import com.instructure.student.databinding.ViewholderFileBinding -import com.instructure.student.fragment.FileListFragment +import com.instructure.student.features.files.list.FileListFragment class FileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -43,7 +43,9 @@ class FileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { root.onClick { callback.onItemClicked(item) } root.onLongClick { overflowButton.performClick() } - if (hasOptions.isNotEmpty()) { + val fileUnavailable = item.isFile && item.url == null + + if (hasOptions.isNotEmpty() && !fileUnavailable) { // User has options overflowButton.setVisible() overflowButton.onClick { callback.onOpenItemMenu(item, overflowButton) } @@ -83,6 +85,10 @@ class FileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fileSize.text = context.resources.getQuantityString(R.plurals.item_count, itemCount, itemCount); fileIcon.setColoredResource(R.drawable.ic_folder_solid, tint) } + + fileFolderLayout.isEnabled = fileUnavailable.not() + fileIcon.alpha = if (fileUnavailable) 0.5f else 1f + textContainer.alpha = if (fileUnavailable) 0.5f else 1f } companion object { diff --git a/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt index 19179863a4..2bd4e88500 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/GradeViewHolder.kt @@ -33,9 +33,9 @@ import com.instructure.pandautils.utils.setGone import com.instructure.pandautils.utils.setVisible import com.instructure.pandautils.utils.textAndIconColor import com.instructure.student.R -import com.instructure.student.adapter.GradesListRecyclerAdapter import com.instructure.student.databinding.ViewholderGradeBinding import com.instructure.student.dialog.WhatIfDialogStyled +import com.instructure.student.features.grades.GradesListRecyclerAdapter import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.util.BinderUtils diff --git a/apps/student/src/main/java/com/instructure/student/holders/ModuleSubHeaderViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/ModuleSubHeaderViewHolder.kt deleted file mode 100644 index 09205a3cff..0000000000 --- a/apps/student/src/main/java/com/instructure/student/holders/ModuleSubHeaderViewHolder.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.holders - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.instructure.canvasapi2.models.ModuleItem -import com.instructure.student.R -import com.instructure.student.databinding.ViewholderSubHeaderModuleBinding -import com.instructure.student.util.BinderUtils - -class ModuleSubHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun bind(moduleItem: ModuleItem, isFirstItem: Boolean, isLastItem: Boolean) = with(ViewholderSubHeaderModuleBinding.bind(itemView)) { - if (ModuleItem.Type.SubHeader.toString().equals(moduleItem.type, ignoreCase = true)) { - subTitle.text = moduleItem.title - } - BinderUtils.updateShadows(isFirstItem, isLastItem, shadowTop, shadowBottom) - } - - companion object { - const val HOLDER_RES_ID = R.layout.viewholder_sub_header_module - } -} diff --git a/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt index 59ae268586..8ed32df3a0 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/PeopleViewHolder.kt @@ -47,7 +47,8 @@ class PeopleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val enrollmentIndex = item.enrollmentIndex if (enrollmentIndex >= 0 && enrollmentIndex < item.enrollments.size) { - role.text = item.enrollments[item.enrollmentIndex].displayType + val roleText = item.enrollments.map { it.displayType }.distinct().joinToString(", ") + role.text = roleText role.setVisible() } else { role.text = "" diff --git a/apps/student/src/main/java/com/instructure/student/holders/QuizViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/QuizViewHolder.kt index e38dcad05e..27297ab373 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/QuizViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/QuizViewHolder.kt @@ -26,12 +26,13 @@ import com.instructure.canvasapi2.utils.NumberHelper import com.instructure.canvasapi2.utils.isValid import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.isVisible +import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.pandautils.utils.setVisible import com.instructure.student.R import com.instructure.student.databinding.ViewholderQuizBinding import com.instructure.student.interfaces.AdapterToFragmentCallback import com.instructure.student.util.BinderUtils -import java.util.* +import java.util.Date class QuizViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -42,7 +43,7 @@ class QuizViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { iconAndTextColor: Int, restrictQuantitativeData: Boolean ) = with(ViewholderQuizBinding.bind(itemView)) { - root.setOnClickListener { adapterToFragmentCallback?.onRowClicked(item, adapterPosition, true) } + root.onClickWithRequireNetwork { adapterToFragmentCallback?.onRowClicked(item, adapterPosition, true) } // Title title.text = item.title diff --git a/apps/student/src/main/java/com/instructure/student/interfaces/CourseAdapterToFragmentCallback.kt b/apps/student/src/main/java/com/instructure/student/interfaces/CourseAdapterToFragmentCallback.kt index 3a0911d020..9d14470177 100644 --- a/apps/student/src/main/java/com/instructure/student/interfaces/CourseAdapterToFragmentCallback.kt +++ b/apps/student/src/main/java/com/instructure/student/interfaces/CourseAdapterToFragmentCallback.kt @@ -17,8 +17,6 @@ package com.instructure.student.interfaces -import com.instructure.canvasapi2.models.AccountNotification -import com.instructure.canvasapi2.models.Conference import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group @@ -29,4 +27,5 @@ interface CourseAdapterToFragmentCallback { fun onCourseSelected(course: Course) fun onEditCourseNickname(course: Course) fun onPickCourseColor(course: Course) + fun onManageOfflineContent(course: Course) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt index bd88b91d10..0cb6b6f633 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsEffectHandler.kt @@ -17,14 +17,10 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails -import com.instructure.canvasapi2.managers.* import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.models.LTITool import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult -import com.instructure.canvasapi2.utils.Failure import com.instructure.canvasapi2.utils.exhaustive -import com.instructure.canvasapi2.utils.weave.StatusCallbackError import com.instructure.pandautils.utils.orDefault import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsSharedEvent import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsView @@ -36,18 +32,22 @@ import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.launch import java.io.File -class SubmissionDetailsEffectHandler : EffectHandler() { +class SubmissionDetailsEffectHandler( + private val repository: SubmissionDetailsRepository +) : EffectHandler() { + + @ObsoleteCoroutinesApi @ExperimentalCoroutinesApi override fun accept(effect: SubmissionDetailsEffect) { when (effect) { is SubmissionDetailsEffect.LoadData -> loadData(effect) is SubmissionDetailsEffect.ShowSubmissionContentType -> { - view?.showSubmissionContent(effect.submissionContentType) + view?.showSubmissionContent(effect.submissionContentType, repository.isOnline()) } is SubmissionDetailsEffect.ShowAudioRecordingView -> { view?.showAudioRecordingView() } - is SubmissionDetailsEffect.ShowVideoRecordingView-> { + is SubmissionDetailsEffect.ShowVideoRecordingView -> { view?.showVideoRecordingView() } is SubmissionDetailsEffect.ShowVideoRecordingPlayback -> { @@ -71,59 +71,64 @@ class SubmissionDetailsEffectHandler : EffectHandler = if (assignmentResult.isSuccess && assignmentResult.dataOrThrow.getSubmissionTypes().contains(Assignment.SubmissionType.ONLINE_UPLOAD)) { + val studioLTIToolResult = if (repository.isOnline() && assignmentResult.containsSubmissionType(Assignment.SubmissionType.ONLINE_UPLOAD)) { effect.courseId.getStudioLTITool() - } else DataResult.Fail(null) + } else { + DataResult.Fail(null) + } // For empty submissions - We need to know if they can make submissions through Studio, only used for file uploads val isStudioEnabled = studioLTIToolResult.dataOrNull != null // Determine if we need to retrieve an authenticated LTI URL based on whether this assignment accepts external tool submissions val ltiToolId = assignmentResult.dataOrNull?.externalToolAttributes?.contentId - val ltiToolResponse = if(ltiToolId != null && ltiToolId != 0L) { + val ltiToolResponse = if (ltiToolId != null && ltiToolId != 0L) { // Use this to create a proper fetch url for the external tool - AssignmentManager.getExternalToolLaunchUrlAsync( - assignmentResult.dataOrNull?.courseId!!, - ltiToolId, assignmentResult.dataOrNull?.id!! - ).await() + repository.getExternalToolLaunchUrl( + assignmentResult.dataOrNull?.courseId!!, + ltiToolId, assignmentResult.dataOrNull?.id!!, + true + ) } else { val assignmentUrl = assignmentResult.dataOrNull?.url - if (assignmentUrl != null && assignmentResult.dataOrNull?.getSubmissionTypes()?.contains(Assignment.SubmissionType.EXTERNAL_TOOL) == true) - SubmissionManager.getLtiFromAuthenticationUrlAsync(assignmentUrl, true).await() - else DataResult.Fail(null) + if (assignmentUrl != null && assignmentResult.containsSubmissionType(Assignment.SubmissionType.EXTERNAL_TOOL)) { + repository.getLtiFromAuthenticationUrl(assignmentUrl, true) + } else { + DataResult.Fail(null) + } } - val ltiTool = if(ltiToolResponse.isSuccess && ltiToolResponse.dataOrNull != null) { - DataResult.Success(ltiToolResponse.dataOrThrow.copy(assignmentId = assignmentResult.dataOrNull?.id!!, courseId = assignmentResult.dataOrNull?.courseId!!)) + val ltiTool = if (ltiToolResponse.dataOrNull != null) { + DataResult.Success( + ltiToolResponse.dataOrThrow.copy( + assignmentId = assignmentResult.dataOrNull?.id!!, + courseId = assignmentResult.dataOrNull?.courseId!! + ) + ) } else { ltiToolResponse } // We need to get the quiz for the empty submission page - val quizResult = if (assignmentResult.dataOrNull?.turnInType == (Assignment.TurnInType.QUIZ) && assignmentResult.dataOrNull?.quizId != 0L) { - try { - QuizManager.getQuizAsync(effect.courseId, assignmentResult.dataOrNull?.quizId!!, true).await() - } catch (e: StatusCallbackError) { - if (e.response?.code() == 401) { - DataResult.Fail(Failure.Authorization(e.response?.message())) - } else { - DataResult.Fail(Failure.Network(e.response?.message())) - } - } - } else null + val quizResult = if (assignmentResult.dataOrNull?.turnInType == Assignment.TurnInType.QUIZ + && assignmentResult.dataOrNull?.quizId != 0L + ) { + repository.getQuiz(effect.courseId, assignmentResult.dataOrNull?.quizId!!, true) + } else { + null + } - val featureFlags = FeaturesManager.getEnabledFeaturesForCourseAsync(effect.courseId, true).await().dataOrNull + val featureFlags = repository.getCourseFeatures(effect.courseId, true).dataOrNull val assignmentEnhancementsEnabled = featureFlags?.contains("assignments_2_student").orDefault() - val restrictQuantitativeData = CourseManager.getCourseSettingsAsync(effect.courseId, true) - .await().dataOrNull?.restrictQuantitativeData.orDefault() + val restrictQuantitativeData = repository.loadCourseSettings(effect.courseId, true)?.restrictQuantitativeData.orDefault() consumer.accept( SubmissionDetailsEvent.DataLoaded( @@ -141,6 +146,10 @@ class SubmissionDetailsEffectHandler : EffectHandler.containsSubmissionType(type: Assignment.SubmissionType): Boolean { + return dataOrNull?.getSubmissionTypes()?.contains(type).orDefault() + } + @ObsoleteCoroutinesApi private fun uploadMediaComment(file: File) { ChannelSource.getChannel().trySend( diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsRepository.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsRepository.kt new file mode 100644 index 0000000000..398355b69c --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsRepository.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.assignmentDetails.submissionDetails + +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsDataSource +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsLocalDataSource +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsNetworkDataSource + +class SubmissionDetailsRepository( + localDataSource: SubmissionDetailsLocalDataSource, + networkDataSource: SubmissionDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getObserveeEnrollments(forceNetwork: Boolean): DataResult> { + return dataSource().getObserveeEnrollments(forceNetwork) + } + + suspend fun getSingleSubmission(courseId: Long, assignmentId: Long, studentId: Long, forceNetwork: Boolean): DataResult { + return dataSource().getSingleSubmission(courseId, assignmentId, studentId, forceNetwork) + } + + suspend fun getAssignment(assignmentId: Long, courseId: Long, forceNetwork: Boolean): DataResult { + return dataSource().getAssignment(assignmentId, courseId, forceNetwork) + } + + suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): DataResult { + return dataSource().getExternalToolLaunchUrl(courseId, externalToolId, assignmentId, forceNetwork) + } + + suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): DataResult { + return dataSource().getLtiFromAuthenticationUrl(url, forceNetwork) + } + + suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): DataResult { + return dataSource().getQuiz(courseId, quizId, forceNetwork) + } + + suspend fun getCourseFeatures(courseId: Long, forceNetwork: Boolean): DataResult> { + return dataSource().getCourseFeatures(courseId, forceNetwork) + } + + suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return dataSource().loadCourseSettings(courseId, forceNetwork) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt index e721630d69..ccbdb0847b 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/SubmissionDetailsUpdate.kt @@ -27,7 +27,6 @@ import com.instructure.student.mobius.common.ui.UpdateInit import com.instructure.student.util.Const import com.spotify.mobius.First import com.spotify.mobius.Next -import java.util.Collections.emptyList class SubmissionDetailsUpdate : UpdateInit() { override fun performInit(model: SubmissionDetailsModel): First { @@ -163,9 +162,9 @@ class SubmissionDetailsUpdate : UpdateInit SubmissionDetailsContentType.NoneContent - Assignment.SubmissionType.ON_PAPER.apiString in assignment?.submissionTypesRaw ?: emptyList() -> SubmissionDetailsContentType.OnPaperContent - Assignment.SubmissionType.EXTERNAL_TOOL.apiString in assignment?.submissionTypesRaw ?: emptyList() -> { + Assignment.SubmissionType.NONE.apiString in assignment?.submissionTypesRaw.orEmpty() -> SubmissionDetailsContentType.NoneContent + Assignment.SubmissionType.ON_PAPER.apiString in assignment?.submissionTypesRaw.orEmpty() -> SubmissionDetailsContentType.OnPaperContent + Assignment.SubmissionType.EXTERNAL_TOOL.apiString in assignment?.submissionTypesRaw.orEmpty() -> { if (assignment?.isAllowedToSubmit == true) SubmissionDetailsContentType.ExternalToolContent(canvasContext, ltiUrl?.url ?: "") else SubmissionDetailsContentType.LockedContent diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt index b9b4934e62..95cf94f8ef 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/DiscussionSubmissionViewFragment.kt @@ -84,14 +84,14 @@ class DiscussionSubmissionViewFragment : Fragment() { // This was an issue when routing a group discussion, with the 'root_discussion_topic_id' // being the course discussion id rather than the group discussion id. (url != discussionUrl && !url.contains("root_discussion_topic_id")) && RouteMatcher.canRouteInternally( - requireContext(), + requireActivity(), url, ApiPrefs.domain, false ) override fun routeInternallyCallback(url: String) { - RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, true) + RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, true) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt index 1f5db61f42..755c40efd6 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/LtiSubmissionViewFragment.kt @@ -26,7 +26,7 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.ParcelableArg import com.instructure.pandautils.utils.StringArg import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.student.R import com.instructure.student.databinding.FragmentLtiSubmissionViewBinding import com.instructure.student.fragment.LtiLaunchFragment @@ -46,9 +46,9 @@ class LtiSubmissionViewFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ViewStyler.themeButton(binding.viewLtiButton) - binding.viewLtiButton.onClick { + binding.viewLtiButton.onClickWithRequireNetwork { val route = LtiLaunchFragment.makeRoute(canvasContext = canvasContext, url = url) - RouteMatcher.route(requireContext(), route) + RouteMatcher.route(requireActivity(), route) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt index 207666f524..2eac130e2c 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/MediaSubmissionViewFragment.kt @@ -71,7 +71,7 @@ class MediaSubmissionViewFragment : Fragment() { submissionMediaPlayerView.findViewById(R.id.fullscreenButton).onClick { exoAgent.flagForResume() val bundle = BaseViewMediaActivity.makeBundle(uri.toString(), thumbnailUrl, contentType, displayName, false) - RouteMatcher.route(requireContext(), Route(bundle, RouteContext.MEDIA)) + RouteMatcher.route(requireActivity(), Route(bundle, RouteContext.MEDIA)) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt index 05a4189e57..2ab33d11cc 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfStudentSubmissionView.kt @@ -17,7 +17,6 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.content import android.annotation.SuppressLint -import android.content.Context import android.graphics.Color import android.util.TypedValue import android.view.Gravity @@ -25,6 +24,7 @@ import android.view.LayoutInflater import android.widget.FrameLayout import android.widget.ImageView import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import com.instructure.annotations.PdfSubmissionView import com.instructure.canvasapi2.managers.CanvaDocsManager @@ -59,12 +59,14 @@ import org.greenrobot.eventbus.ThreadMode @SuppressLint("ViewConstructor") class PdfStudentSubmissionView( - context: Context, - private val pdfUrl: String, - private val fragmentManager: FragmentManager, - private val studentAnnotationSubmit: Boolean = false, - private val studentAnnotationView: Boolean = false, -) : PdfSubmissionView(context, studentAnnotationView), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { + private val activity: FragmentActivity, + private val pdfUrl: String, + private val fragmentManager: FragmentManager, + private val studentAnnotationSubmit: Boolean = false, + private val studentAnnotationView: Boolean = false, +) : PdfSubmissionView( + activity, studentAnnotationView +), AnnotationManager.OnAnnotationCreationModeChangeListener, AnnotationManager.OnAnnotationEditingModeChangeListener { private val binding: ViewPdfStudentSubmissionBinding @@ -88,8 +90,16 @@ class PdfStudentSubmissionView( override fun enableViewPager() {} override fun setIsCurrentlyAnnotating(boolean: Boolean) {} - override fun showAnnotationComments(commentList: ArrayList, headAnnotationId: String, docSession: DocSession, apiValues: ApiValues) { - if (isAttachedToWindow) RouteMatcher.route(context, AnnotationCommentListFragment.makeRoute(commentList, headAnnotationId, docSession, apiValues, ApiPrefs.user!!.id, !studentAnnotationView)) + override fun showAnnotationComments( + commentList: ArrayList, + headAnnotationId: String, + docSession: DocSession, + apiValues: ApiValues + ) { + if (isAttachedToWindow) RouteMatcher.route( + activity, + AnnotationCommentListFragment.makeRoute(commentList, headAnnotationId, docSession, apiValues, ApiPrefs.user!!.id, !studentAnnotationView) + ) } override fun showFileError() { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfSubmissionViewFragment.kt index 5ad05191da..72cf8ad4bc 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/PdfSubmissionViewFragment.kt @@ -28,7 +28,7 @@ class PdfSubmissionViewFragment : Fragment() { private var pdfUrl by StringArg() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return PdfStudentSubmissionView(requireContext(), pdfUrl, childFragmentManager) + return PdfStudentSubmissionView(requireActivity(), pdfUrl, childFragmentManager) } companion object { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt index 66a96df2e0..26d9e6e011 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/TextSubmissionViewFragment.kt @@ -65,10 +65,10 @@ class TextSubmissionViewFragment : Fragment() { override fun onPageStartedCallback(webView: WebView, url: String) = Unit override fun onPageFinishedCallback(webView: WebView, url: String) = Unit override fun canRouteInternallyDelegate(url: String) = - RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, false) + RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, false) override fun routeInternallyCallback(url: String) { - RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, true) + RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, true) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt index 86d2b13d50..e0bfd1aed1 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/content/emptySubmission/ui/SubmissionDetailsEmptyContentView.kt @@ -15,7 +15,6 @@ */ package com.instructure.student.mobius.assignmentDetails.submissionDetails.content.emptySubmission.ui -import android.app.Activity import android.app.Dialog import android.content.Intent import android.content.res.ColorStateList @@ -25,12 +24,13 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.pandautils.utils.setHidden import com.instructure.pandautils.utils.setVisible import com.instructure.pandautils.views.RecordingMediaType @@ -69,7 +69,7 @@ class SubmissionDetailsEmptyContentView( } override fun onConnect(output: Consumer) { - binding.submitButton.onClick { output.accept(SubmissionDetailsEmptyContentEvent.SubmitAssignmentClicked) } + binding.submitButton.onClickWithRequireNetwork { output.accept(SubmissionDetailsEmptyContentEvent.SubmitAssignmentClicked) } } override fun render(state: SubmissionDetailsEmptyContentViewState) { @@ -127,17 +127,17 @@ class SubmissionDetailsEmptyContentView( fun showOnlineTextEntryView(assignmentId: Long, assignmentName: String?, submittedText: String? = null, isFailure: Boolean = false) { logEventWithOrigin(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED) - RouteMatcher.route(context, TextSubmissionUploadFragment.makeRoute(canvasContext, assignmentId, assignmentName, submittedText, isFailure)) + RouteMatcher.route(activity as FragmentActivity, TextSubmissionUploadFragment.makeRoute(canvasContext, assignmentId, assignmentName, submittedText, isFailure)) } fun showOnlineUrlEntryView(assignmentId: Long, assignmentName: String?, canvasContext: CanvasContext, submittedUrl: String? = null) { logEventWithOrigin(AnalyticsEventConstants.SUBMIT_ONLINEURL_SELECTED) - RouteMatcher.route(context, UrlSubmissionUploadFragment.makeRoute(canvasContext, assignmentId, assignmentName, submittedUrl)) + RouteMatcher.route(activity as FragmentActivity, UrlSubmissionUploadFragment.makeRoute(canvasContext, assignmentId, assignmentName, submittedUrl)) } fun showLTIView(canvasContext: CanvasContext, title: String, ltiTool: LTITool? = null) { logEventWithOrigin(AnalyticsEventConstants.ASSIGNMENT_LAUNCHLTI_SELECTED) - RouteMatcher.route(context, LtiLaunchFragment.makeRoute( + RouteMatcher.route(activity as FragmentActivity, LtiLaunchFragment.makeRoute( canvasContext, ltiTool?.url ?: "", title, @@ -148,12 +148,12 @@ class SubmissionDetailsEmptyContentView( fun showQuizStartView(canvasContext: CanvasContext, quiz: Quiz) { logEventWithOrigin(AnalyticsEventConstants.ASSIGNMENT_DETAIL_QUIZLAUNCH) - RouteMatcher.route(context, BasicQuizViewFragment.makeRoute(canvasContext, quiz, quiz.url!!)) + RouteMatcher.route(activity as FragmentActivity, BasicQuizViewFragment.makeRoute(canvasContext, quiz, quiz.url!!)) } fun showDiscussionDetailView(canvasContext: CanvasContext, discussionTopicHeaderId: Long) { logEventWithOrigin(AnalyticsEventConstants.ASSIGNMENT_DETAIL_DISCUSSIONLAUNCH) - RouteMatcher.route(context, DiscussionRouterFragment.makeRoute(canvasContext, discussionTopicHeaderId)) + RouteMatcher.route(activity as FragmentActivity, DiscussionRouterFragment.makeRoute(canvasContext, discussionTopicHeaderId)) } fun showMediaRecordingView() { @@ -179,7 +179,7 @@ class SubmissionDetailsEmptyContentView( private fun showStudioUploadView(assignment: Assignment, ltiUrl: String, studioLtiToolName: String) { logEventWithOrigin(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED) - RouteMatcher.route(context, StudioWebViewFragment.makeRoute(canvasContext, ltiUrl, studioLtiToolName, true, assignment)) + RouteMatcher.route(activity as FragmentActivity, StudioWebViewFragment.makeRoute(canvasContext, ltiUrl, studioLtiToolName, true, assignment)) } fun showAudioRecordingView() { @@ -211,23 +211,23 @@ class SubmissionDetailsEmptyContentView( fun showFileUploadView(assignment: Assignment) { logEventWithOrigin(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED) - RouteMatcher.route(context, PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission)) + RouteMatcher.route(activity as FragmentActivity, PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission)) } fun showQuizOrDiscussionView(url: String) { - if (!RouteMatcher.canRouteInternally(context, url, ApiPrefs.domain, true)) { + if (!RouteMatcher.canRouteInternally(activity as FragmentActivity, url, ApiPrefs.domain, true)) { val intent = Intent(context, InternalWebViewActivity::class.java) context.startActivity(intent) } } fun launchFilePickerView(uri: Uri, course: Course, assignment: Assignment) { - RouteMatcher.route(context, PickerSubmissionUploadFragment.makeRoute(course, assignment, uri)) + RouteMatcher.route(activity as FragmentActivity, PickerSubmissionUploadFragment.makeRoute(course, assignment, uri)) } fun returnToAssignmentDetails() { // Not run on main thread of fragment host by default, so force it to run on UI thread - (context as Activity).runOnUiThread { (context as Activity).onBackPressed() } + activity.runOnUiThread { activity.onBackPressed() } } fun showStudentAnnotationView(assignment: Assignment) { @@ -236,7 +236,7 @@ class SubmissionDetailsEmptyContentView( val submissionId = assignment.submission?.id if (submissionId != null) { RouteMatcher.route( - context, + activity as FragmentActivity, AnnotationSubmissionUploadFragment.makeRoute( canvasContext, assignment.annotatableAttachmentId, diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsDataSource.kt new file mode 100644 index 0000000000..f15dbf698a --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsDataSource.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource + +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult + +interface SubmissionDetailsDataSource { + + suspend fun getObserveeEnrollments(forceNetwork: Boolean): DataResult> + + suspend fun getSingleSubmission(courseId: Long, assignmentId: Long, studentId: Long, forceNetwork: Boolean): DataResult + + suspend fun getAssignment(assignmentId: Long, courseId: Long, forceNetwork: Boolean): DataResult + + suspend fun getExternalToolLaunchUrl(courseId: Long, externalToolId: Long, assignmentId: Long, forceNetwork: Boolean): DataResult + + suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): DataResult + + suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): DataResult + + suspend fun getCourseFeatures(courseId: Long, forceNetwork: Boolean): DataResult> + + suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsLocalDataSource.kt new file mode 100644 index 0000000000..0ef01a53cd --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsLocalDataSource.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource + +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade + +class SubmissionDetailsLocalDataSource( + private val enrollmentFacade: EnrollmentFacade, + private val submissionFacade: SubmissionFacade, + private val assignmentFacade: AssignmentFacade, + private val quizDao: QuizDao, + private val courseFeaturesDao: CourseFeaturesDao, + private val courseSettingsDao: CourseSettingsDao +) : SubmissionDetailsDataSource { + + override suspend fun getObserveeEnrollments(forceNetwork: Boolean): DataResult> { + return DataResult.Success(enrollmentFacade.getAllEnrollments()) + } + + override suspend fun getSingleSubmission(courseId: Long, assignmentId: Long, studentId: Long, forceNetwork: Boolean): DataResult { + val submission = submissionFacade.findByAssignmentId(assignmentId) + return submission?.let { DataResult.Success(it) } ?: DataResult.Fail() + } + + override suspend fun getAssignment(assignmentId: Long, courseId: Long, forceNetwork: Boolean): DataResult { + val assignment = assignmentFacade.getAssignmentById(assignmentId) + return assignment?.let { DataResult.Success(it) } ?: DataResult.Fail() + } + + override suspend fun getExternalToolLaunchUrl( + courseId: Long, + externalToolId: Long, + assignmentId: Long, + forceNetwork: Boolean + ): DataResult { + return DataResult.Fail() + } + + override suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): DataResult { + return DataResult.Fail() + } + + override suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): DataResult { + val quiz = quizDao.findById(quizId)?.toApiModel() + return quiz?.let { DataResult.Success(it) } ?: DataResult.Fail() + } + + override suspend fun getCourseFeatures(courseId: Long, forceNetwork: Boolean): DataResult> { + val features = courseFeaturesDao.findByCourseId(courseId)?.features + return features?.let { DataResult.Success(it) } ?: DataResult.Fail() + } + + override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return courseSettingsDao.findByCourseId(courseId)?.toApiModel() + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..5fddf526b1 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/datasource/SubmissionDetailsNetworkDataSource.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource + +import com.instructure.canvasapi2.apis.* +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class SubmissionDetailsNetworkDataSource( + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, + private val submissionApi: SubmissionAPI.SubmissionInterface, + private val assignmentApi: AssignmentAPI.AssignmentInterface, + private val quizApi: QuizAPI.QuizInterface, + private val featuresApi: FeaturesAPI.FeaturesInterface, + private val courseApi: CourseAPI.CoursesInterface +) : SubmissionDetailsDataSource { + + override suspend fun getObserveeEnrollments(forceNetwork: Boolean): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return enrollmentApi.firstPageObserveeEnrollments(params).depaginate { + enrollmentApi.getNextPage(it, params) + } + } + + override suspend fun getSingleSubmission(courseId: Long, assignmentId: Long, studentId: Long, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return submissionApi.getSingleSubmission(courseId, assignmentId, studentId, params) + } + + override suspend fun getAssignment(assignmentId: Long, courseId: Long, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return assignmentApi.getAssignment(courseId, assignmentId, params) + } + + override suspend fun getExternalToolLaunchUrl( + courseId: Long, + externalToolId: Long, + assignmentId: Long, + forceNetwork: Boolean + ): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return assignmentApi.getExternalToolLaunchUrl(courseId, externalToolId, assignmentId, restParams = params) + } + + override suspend fun getLtiFromAuthenticationUrl(url: String, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return submissionApi.getLtiFromAuthenticationUrl(url, params) + } + + override suspend fun getQuiz(courseId: Long, quizId: Long, forceNetwork: Boolean): DataResult { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return quizApi.getQuiz(courseId, quizId, params) + } + + override suspend fun getCourseFeatures(courseId: Long, forceNetwork: Boolean): DataResult> { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return featuresApi.getEnabledFeaturesForCourse(courseId, params) + } + + override suspend fun loadCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return courseApi.getCourseSettings(courseId, restParams).dataOrNull + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt index 5ba7181902..385e961982 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/SubmissionCommentsView.kt @@ -21,6 +21,7 @@ import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Attachment @@ -89,7 +90,7 @@ class SubmissionCommentsView( } // Set up add files button - addFileButton.onClick { + addFileButton.onClickWithRequireNetwork { consumer?.accept(SubmissionCommentsEvent.AddFilesClicked) } @@ -138,7 +139,7 @@ class SubmissionCommentsView( fun showFilePicker(canvasContext: CanvasContext, assignment: Assignment, attemptId: Long?) { RouteMatcher.route( - context, + activity as FragmentActivity, PickerSubmissionUploadFragment.makeRoute( canvasContext, assignment, @@ -159,7 +160,7 @@ class SubmissionCommentsView( } fun openMedia(canvasContext: CanvasContext, contentType: String, url: String, fileName: String) { - (context as? BaseRouterActivity)?.openMedia(canvasContext, contentType, url, fileName) + (activity as? BaseRouterActivity)?.openMedia(canvasContext, contentType, url, fileName) } fun showPermissionDeniedToast() { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentAttachmentsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentAttachmentsView.kt index 117a170004..7f32d3f286 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentAttachmentsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentAttachmentsView.kt @@ -23,7 +23,7 @@ import android.view.LayoutInflater import android.widget.LinearLayout import com.instructure.canvasapi2.models.Attachment import com.instructure.pandautils.utils.iconRes -import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.onClickWithRequireNetwork import com.instructure.student.databinding.ViewCommentAttachmentBinding @SuppressLint("ViewConstructor") @@ -41,7 +41,7 @@ class CommentAttachmentsView( binding.iconImageView.setImageResource(attachment.iconRes) binding.iconImageView.setColorFilter(tint) binding.attachmentNameTextView.text = attachment.displayName - binding.root.onClick { onClicked(attachment) } + binding.root.onClickWithRequireNetwork { onClicked(attachment) } addView(binding.root) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt index b9223505be..1844a5672b 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt @@ -29,10 +29,7 @@ import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.MediaComment import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.prettyPrint -import com.instructure.pandautils.utils.DP -import com.instructure.pandautils.utils.iconRes -import com.instructure.pandautils.utils.onClick -import com.instructure.pandautils.utils.setGone +import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.databinding.ViewCommentSubmissionAttachmentBinding @@ -132,7 +129,7 @@ class CommentSubmissionView( binding.iconImageView.setImageResource(attachment.iconRes) binding.titleTextView.text = attachment.displayName binding.subtitleTextView.text = Formatter.formatFileSize(context, attachment.size) - binding.root.onClick { onAttachmentClicked(submission, attachment) } + binding.root.onClickWithRequireNetwork { onAttachmentClicked(submission, attachment) } if (index > 0) { (binding.root.layoutParams as LayoutParams).topMargin = context.DP(4).toInt() } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt index 8c44fd83c7..f9018ce7f9 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/files/ui/SubmissionFilesAdapter.kt @@ -17,11 +17,16 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.files.ui import android.content.res.ColorStateList +import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.instructure.canvasapi2.utils.isValid import com.instructure.canvasapi2.utils.validOrNull import com.instructure.pandautils.utils.setVisible @@ -62,7 +67,25 @@ internal class SubmissionFilesHolder(view: View) : RecyclerView.ViewHolder(view) // Thumbnail Glide.with(root.context).clear(thumbnail) thumbnail.setVisible(data.thumbnailUrl.isValid()) - data.thumbnailUrl.validOrNull()?.let { Glide.with(root.context).load(it).into(thumbnail) } + data.thumbnailUrl.validOrNull()?.let { + Glide.with(root.context).load(it).listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + fileIcon.setVisible(true) + thumbnail.setVisible(false) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + return false + } + }).into(thumbnail) + } // Title fileName.text = data.name diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricDescriptionFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricDescriptionFragment.kt index 8491e64dba..c86c64f07c 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricDescriptionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricDescriptionFragment.kt @@ -72,11 +72,11 @@ class SubmissionRubricDescriptionFragment : DialogFragment() { override fun onPageStartedCallback(webView: WebView, url: String) {} override fun onPageFinishedCallback(webView: WebView, url: String) {} override fun canRouteInternallyDelegate(url: String): Boolean { - return RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, false) + return RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, false) } override fun routeInternallyCallback(url: String) { - RouteMatcher.canRouteInternally(requireContext(), url, ApiPrefs.domain, true) + RouteMatcher.canRouteInternally(requireActivity(), url, ApiPrefs.domain, true) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricView.kt index 7cc5a975a4..d6467c6473 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/rubric/ui/SubmissionRubricView.kt @@ -18,6 +18,7 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.drawe import android.view.LayoutInflater import android.view.ViewGroup +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import com.instructure.pandautils.adapters.BasicItemCallback import com.instructure.pandautils.adapters.BasicRecyclerAdapter @@ -67,7 +68,7 @@ class SubmissionRubricView( override fun applyTheme() = Unit fun displayCriterionDescription(title: String, description: String) { - RouteMatcher.route(context, SubmissionRubricDescriptionFragment.makeRoute(title, description)) + RouteMatcher.route(activity as FragmentActivity, SubmissionRubricDescriptionFragment.makeRoute(title, description)) } } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt index 5fee21fcdf..2f7277fb89 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsFragment.kt @@ -18,14 +18,11 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui import android.view.LayoutInflater import android.view.ViewGroup -import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam -import com.instructure.interactions.router.Route -import com.instructure.interactions.router.RouterParams import com.instructure.pandautils.analytics.SCREEN_VIEW_SUBMISSION_DETAILS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.* @@ -40,8 +37,8 @@ import com.instructure.student.mobius.common.ui.MobiusFragment @ScreenView(SCREEN_VIEW_SUBMISSION_DETAILS) @PageView(url = "{canvasContext}/assignments/{assignmentId}/submissions") -class SubmissionDetailsFragment : - MobiusFragment() { +abstract class SubmissionDetailsFragment : MobiusFragment() { val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) @@ -50,7 +47,7 @@ class SubmissionDetailsFragment : val isObserver by BooleanArg(key = Const.IS_OBSERVER, default = false) private val initialSelectedSubmissionAttempt by LongArg(key = Const.SUBMISSION_ATTEMPT) - override fun makeEffectHandler() = SubmissionDetailsEffectHandler() + override fun makeEffectHandler() = SubmissionDetailsEffectHandler(getRepository()) override fun makeUpdate() = SubmissionDetailsUpdate() @@ -96,39 +93,5 @@ class SubmissionDetailsFragment : } ) - companion object { - fun makeRoute(course: CanvasContext, assignmentId: Long, isObserver: Boolean = false, initialSelectedSubmissionAttempt: Long? = null): Route { - val bundle = course.makeBundle { - putLong(Const.ASSIGNMENT_ID, assignmentId) - putBoolean(Const.IS_OBSERVER, isObserver) - initialSelectedSubmissionAttempt?.let { putLong(Const.SUBMISSION_ATTEMPT, it) } - } - return Route(null, SubmissionDetailsFragment::class.java, course, bundle) - } - - fun validRoute(route: Route): Boolean { - return route.canvasContext is Course && - (route.arguments.containsKey(Const.ASSIGNMENT_ID) || - route.paramsHash.containsKey(RouterParams.ASSIGNMENT_ID)) - } - - fun newInstance(route: Route): SubmissionDetailsFragment? { - if (!validRoute(route)) return null - - // If routed from a URL, set the bundle's assignment ID from the url value - if (route.paramsHash.containsKey(RouterParams.ASSIGNMENT_ID)) { - val assignmentId = route.paramsHash[RouterParams.ASSIGNMENT_ID]?.toLong() ?: -1 - route.arguments.putLong(Const.ASSIGNMENT_ID, assignmentId) - } - - if (route.paramsHash.containsKey(Const.SUBMISSION_ATTEMPT)) { - val submissionAttempt = route.paramsHash[Const.SUBMISSION_ATTEMPT]?.toLong() ?: -1 - route.arguments.putLong(Const.SUBMISSION_ATTEMPT, submissionAttempt) - } - - return SubmissionDetailsFragment().withArgs(route.arguments) - } - - } - + abstract fun getRepository(): SubmissionDetailsRepository } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt new file mode 100644 index 0000000000..a6e833eb71 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsRepositoryFragment.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui + +import android.os.Bundle +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.interactions.router.Route +import com.instructure.interactions.router.RouterParams +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.makeBundle +import com.instructure.pandautils.utils.withArgs +import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsRepository +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class SubmissionDetailsRepositoryFragment : SubmissionDetailsFragment() { + + @Inject + lateinit var submissionDetailsRepository: SubmissionDetailsRepository + + override fun getRepository() = submissionDetailsRepository + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = false + } + + companion object { + fun makeRoute( + course: CanvasContext, + assignmentId: Long, + isObserver: Boolean = false, + initialSelectedSubmissionAttempt: Long? = null + ): Route { + val bundle = course.makeBundle { + putLong(Const.ASSIGNMENT_ID, assignmentId) + putBoolean(Const.IS_OBSERVER, isObserver) + initialSelectedSubmissionAttempt?.let { putLong(Const.SUBMISSION_ATTEMPT, it) } + } + return Route(null, SubmissionDetailsRepositoryFragment::class.java, course, bundle) + } + + fun validRoute(route: Route): Boolean { + return route.canvasContext is Course && + (route.arguments.containsKey(Const.ASSIGNMENT_ID) || + route.paramsHash.containsKey(RouterParams.ASSIGNMENT_ID)) + } + + fun newInstance(route: Route): SubmissionDetailsRepositoryFragment? { + if (!validRoute(route)) return null + + // If routed from a URL, set the bundle's assignment ID from the url value + if (route.paramsHash.containsKey(RouterParams.ASSIGNMENT_ID)) { + val assignmentId = route.paramsHash[RouterParams.ASSIGNMENT_ID]?.toLong() ?: -1 + route.arguments.putLong(Const.ASSIGNMENT_ID, assignmentId) + } + + if (route.paramsHash.containsKey(Const.SUBMISSION_ATTEMPT)) { + val submissionAttempt = route.paramsHash[Const.SUBMISSION_ATTEMPT]?.toLong() ?: -1 + route.arguments.putLong(Const.SUBMISSION_ATTEMPT, submissionAttempt) + } + + return SubmissionDetailsRepositoryFragment().withArgs(route.arguments) + } + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt index 5bb1ea8c72..b89ef03fa0 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/ui/SubmissionDetailsView.kt @@ -17,7 +17,6 @@ package com.instructure.student.mobius.assignmentDetails.submissionDetails.ui -import android.app.Activity import android.net.Uri import android.os.Bundle import android.view.LayoutInflater @@ -27,6 +26,7 @@ import android.widget.AdapterView import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import com.google.android.material.tabs.TabLayout import com.instructure.canvasapi2.models.CanvasContext @@ -44,6 +44,7 @@ import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.RecordingMediaType import com.instructure.student.R import com.instructure.student.databinding.FragmentSubmissionDetailsBinding +import com.instructure.student.features.modules.progression.NotAvailableOfflineFragment import com.instructure.student.fragment.ViewImageFragment import com.instructure.student.fragment.ViewUnsupportedFileFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType @@ -75,8 +76,8 @@ class SubmissionDetailsView( override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabReselected(tab: TabLayout.Tab?) = onTabSelected(tab) override fun onTabSelected(tab: TabLayout.Tab?) { - if (binding.slidingUpPanelLayout?.panelState == SlidingUpPanelLayout.PanelState.COLLAPSED) { - binding.slidingUpPanelLayout?.panelState = SlidingUpPanelLayout.PanelState.ANCHORED + if (binding.slidingUpPanelLayout.panelState == SlidingUpPanelLayout.PanelState.COLLAPSED) { + binding.slidingUpPanelLayout.panelState = SlidingUpPanelLayout.PanelState.ANCHORED } binding.drawerViewPager.hideKeyboard() logTabSelected(tab?.position) @@ -92,7 +93,7 @@ class SubmissionDetailsView( } init { - binding.toolbar.setupAsBackButton { (context as? Activity)?.onBackPressed() } + binding.toolbar.setupAsBackButton { activity.onBackPressed() } binding.retryButton.onClick { consumer?.accept(SubmissionDetailsEvent.RefreshRequested) } binding.drawerViewPager.offscreenPageLimit = 3 binding.drawerViewPager.adapter = drawerPagerAdapter @@ -168,7 +169,7 @@ class SubmissionDetailsView( } override fun applyTheme() { - ViewStyler.themeToolbarColored(context as Activity, binding.toolbar, canvasContext) + ViewStyler.themeToolbarColored(activity, binding.toolbar, canvasContext) } override fun onConnect(output: Consumer) { @@ -233,9 +234,9 @@ class SubmissionDetailsView( } } - fun showSubmissionContent(type: SubmissionDetailsContentType) { + fun showSubmissionContent(type: SubmissionDetailsContentType, isOnline: Boolean) { fragmentManager.beginTransaction().apply { - replace(R.id.submissionContent, getFragmentForContent(type)) + replace(R.id.submissionContent, getFragmentForContent(type, isOnline)) commitAllowingStateLoss() } } @@ -271,7 +272,7 @@ class SubmissionDetailsView( fun showVideoRecordingPlayback(file: File) { val bundle = BaseViewMediaActivity.makeBundle(file, "video", context.getString(R.string.videoCommentReplay), true) - RouteMatcher.route(context, Route(bundle, RouteContext.MEDIA)) + RouteMatcher.route(activity as FragmentActivity, Route(bundle, RouteContext.MEDIA)) } fun showVideoRecordingPlaybackError() { @@ -282,28 +283,60 @@ class SubmissionDetailsView( Toast.makeText(context, R.string.errorSubmittingMediaComment, Toast.LENGTH_SHORT).show() } - private fun getFragmentForContent(type: SubmissionDetailsContentType): Fragment { + private fun getFragmentForContent(type: SubmissionDetailsContentType, isOnline: Boolean): Fragment { return when (type) { - is SubmissionDetailsContentType.NoSubmissionContent -> SubmissionDetailsEmptyContentFragment.newInstance(type.canvasContext as Course, type.assignment, type.isStudioEnabled, type.quiz, type.studioLTITool, type.isObserver, type.ltiTool) + is SubmissionDetailsContentType.NoSubmissionContent -> SubmissionDetailsEmptyContentFragment.newInstance( + type.canvasContext as Course, + type.assignment, + type.isStudioEnabled, + type.quiz, + type.studioLTITool, + type.isObserver, + type.ltiTool + ) is SubmissionDetailsContentType.UrlContent -> UrlSubmissionViewFragment.newInstance(type.url, type.previewUrl) - is SubmissionDetailsContentType.QuizContent -> QuizSubmissionViewFragment.newInstance(type.url) + is SubmissionDetailsContentType.QuizContent -> getFragmentWithOnlineCheck(QuizSubmissionViewFragment.newInstance(type.url), isOnline) is SubmissionDetailsContentType.TextContent -> TextSubmissionViewFragment.newInstance(type.text) - is SubmissionDetailsContentType.DiscussionContent -> DiscussionSubmissionViewFragment.newInstance(type.previewUrl ?: "") - is SubmissionDetailsContentType.PdfContent -> PdfSubmissionViewFragment.newInstance(type.url) + is SubmissionDetailsContentType.DiscussionContent -> getFragmentWithOnlineCheck( + DiscussionSubmissionViewFragment.newInstance(type.previewUrl.orEmpty()), + isOnline + ) + is SubmissionDetailsContentType.PdfContent -> getFragmentWithOnlineCheck(PdfSubmissionViewFragment.newInstance(type.url), isOnline) is SubmissionDetailsContentType.ExternalToolContent -> LtiSubmissionViewFragment.newInstance(type) - is SubmissionDetailsContentType.MediaContent -> MediaSubmissionViewFragment.newInstance(type) + is SubmissionDetailsContentType.MediaContent -> getFragmentWithOnlineCheck(MediaSubmissionViewFragment.newInstance(type), isOnline) is SubmissionDetailsContentType.OtherAttachmentContent -> ViewUnsupportedFileFragment.newInstance( uri = Uri.parse(type.attachment.url), - displayName = type.attachment.displayName ?: "", - contentType = type.attachment.contentType ?: "", + displayName = type.attachment.displayName.orEmpty(), + contentType = type.attachment.contentType.orEmpty(), previewUri = type.attachment.previewUrl?.let { Uri.parse(it) }, fallbackIcon = R.drawable.ic_attachment ) - is SubmissionDetailsContentType.ImageContent -> ViewImageFragment.newInstance(type.title, Uri.parse(type.url), type.contentType, false) - SubmissionDetailsContentType.NoneContent -> SubmissionMessageFragment.newInstance(title = R.string.noOnlineSubmissions, subtitle = R.string.noneContentMessage) - SubmissionDetailsContentType.OnPaperContent -> SubmissionMessageFragment.newInstance(title = R.string.noOnlineSubmissions, subtitle = R.string.onPaperContentMessage) - SubmissionDetailsContentType.LockedContent -> SubmissionMessageFragment.newInstance(title = R.string.submissionDetailsAssignmentLocked, subtitle = R.string.could_not_route_locked) - is SubmissionDetailsContentType.StudentAnnotationContent -> AnnotationSubmissionViewFragment.newInstance(type.subissionId, type.submissionAttempt) + is SubmissionDetailsContentType.ImageContent -> getFragmentWithOnlineCheck( + ViewImageFragment.newInstance( + type.title, + Uri.parse(type.url), + type.contentType, + false + ), isOnline + ) + SubmissionDetailsContentType.NoneContent -> SubmissionMessageFragment.newInstance( + title = R.string.noOnlineSubmissions, + subtitle = R.string.noneContentMessage + ) + SubmissionDetailsContentType.OnPaperContent -> SubmissionMessageFragment.newInstance( + title = R.string.noOnlineSubmissions, + subtitle = R.string.onPaperContentMessage + ) + SubmissionDetailsContentType.LockedContent -> SubmissionMessageFragment.newInstance( + title = R.string.submissionDetailsAssignmentLocked, + subtitle = R.string.could_not_route_locked + ) + is SubmissionDetailsContentType.StudentAnnotationContent -> getFragmentWithOnlineCheck( + AnnotationSubmissionViewFragment.newInstance( + type.subissionId, + type.submissionAttempt + ), isOnline + ) is SubmissionDetailsContentType.UnsupportedContent -> { // Users shouldn't get here, but we'll handle the case and send up some analytics if they do val bundle = Bundle().apply { @@ -323,6 +356,14 @@ class SubmissionDetailsView( } } + private fun getFragmentWithOnlineCheck(fragmentIfOnline: Fragment, isOnline: Boolean): Fragment { + return if (isOnline) { + fragmentIfOnline + } else { + NotAvailableOfflineFragment.newInstance(NotAvailableOfflineFragment.makeRoute(canvasContext, showToolbar = false)) + } + } + companion object { private const val ANCHOR_POINT = 0.5f } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ConferenceDetailsEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ConferenceDetailsEffectHandler.kt index 7e41babf57..1960bd07df 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ConferenceDetailsEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ConferenceDetailsEffectHandler.kt @@ -16,22 +16,20 @@ */ package com.instructure.student.mobius.conferences.conference_details -import com.instructure.canvasapi2.managers.ConferenceManager -import com.instructure.canvasapi2.managers.OAuthManager -import com.instructure.canvasapi2.models.AuthenticatedSession import com.instructure.canvasapi2.utils.exhaustive -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.student.mobius.common.ui.EffectHandler import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsView import kotlinx.coroutines.delay import kotlinx.coroutines.launch -class ConferenceDetailsEffectHandler : EffectHandler() { +class ConferenceDetailsEffectHandler( + private val repository: ConferenceDetailsRepository +) : EffectHandler() { override fun accept(effect: ConferenceDetailsEffect) { when (effect) { is ConferenceDetailsEffect.JoinConference -> joinConference(effect) is ConferenceDetailsEffect.RefreshData -> refreshData(effect) - ConferenceDetailsEffect.DisplayRefreshError -> view?.displayRefreshError() + is ConferenceDetailsEffect.DisplayRefreshError -> view?.displayRefreshError() is ConferenceDetailsEffect.ShowRecording -> showRecording(effect) }.exhaustive } @@ -49,7 +47,7 @@ class ConferenceDetailsEffectHandler : EffectHandler { OAuthManager.getAuthenticatedSession(url, it) } + val authSession = repository.getAuthenticatedSession(url) url = authSession.sessionUrl } catch (e: Throwable) { // Try launching without authenticated URL @@ -63,7 +61,7 @@ class ConferenceDetailsEffectHandler : EffectHandler. + * + */ + +package com.instructure.student.mobius.conferences.conference_details + +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsDataSource +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsLocalDataSource +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsNetworkDataSource + +class ConferenceDetailsRepository( + localDataSource: ConferenceDetailsLocalDataSource, + private val networkDataSource: ConferenceDetailsNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getConferencesForContext( + canvasContext: CanvasContext, forceNetwork: Boolean + ): DataResult> { + return dataSource().getConferencesForContext(canvasContext, forceNetwork) + } + + suspend fun getAuthenticatedSession(targetUrl: String): AuthenticatedSession { + return networkDataSource.getAuthenticatedSession(targetUrl) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsDataSource.kt new file mode 100644 index 0000000000..d2c2fbb062 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsDataSource.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult + +interface ConferenceDetailsDataSource { + + suspend fun getConferencesForContext(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsLocalDataSource.kt new file mode 100644 index 0000000000..612df11711 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsLocalDataSource.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.ConferenceFacade + +class ConferenceDetailsLocalDataSource( + private val conferenceFacade: ConferenceFacade +) : ConferenceDetailsDataSource { + + override suspend fun getConferencesForContext(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return DataResult.Success(conferenceFacade.getConferencesByCourseId(canvasContext.id)) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsNetworkDataSource.kt new file mode 100644 index 0000000000..48a4888b5d --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/datasource/ConferenceDetailsNetworkDataSource.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_details.datasource + +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class ConferenceDetailsNetworkDataSource( + private val conferencesApi: ConferencesApi.ConferencesInterface, + private val oAuthApi: OAuthAPI.OAuthInterface +) : ConferenceDetailsDataSource { + + override suspend fun getConferencesForContext( + canvasContext: CanvasContext, forceNetwork: Boolean + ): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return conferencesApi.getConferencesForContext(canvasContext.toAPIString().drop(1), params).map { + it.conferences + }.depaginate { url -> + conferencesApi.getNextPage(url, params).map { it.conferences } + } + } + + suspend fun getAuthenticatedSession(targetUrl: String): AuthenticatedSession { + val params = RestParams(isForceReadFromNetwork = true) + + return oAuthApi.getAuthenticatedSession(targetUrl, params).dataOrThrow + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsFragment.kt index a99e308aac..59ead89e2e 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsFragment.kt @@ -20,65 +20,35 @@ import android.view.LayoutInflater import android.view.ViewGroup import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference -import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam -import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_CONFERENCE_DETAILS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.makeBundle -import com.instructure.pandautils.utils.withArgs import com.instructure.student.databinding.FragmentConferenceDetailsBinding import com.instructure.student.mobius.common.ui.MobiusFragment import com.instructure.student.mobius.conferences.conference_details.* -@PageView(url = "{canvasContext}/conferences/{conferenceId}") @ScreenView(SCREEN_VIEW_CONFERENCE_DETAILS) -class ConferenceDetailsFragment : - MobiusFragment() { +abstract class ConferenceDetailsFragment : MobiusFragment() { val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) val conference by ParcelableArg(key = Const.CONFERENCE) - override fun makeUpdate() = - ConferenceDetailsUpdate() + override fun makeUpdate() = ConferenceDetailsUpdate() - override fun makePresenter() = - ConferenceDetailsPresenter + override fun makePresenter() = ConferenceDetailsPresenter - override fun makeEffectHandler() = - ConferenceDetailsEffectHandler() + override fun makeEffectHandler() = ConferenceDetailsEffectHandler(getRepository()) - override fun makeInitModel() = - ConferenceDetailsModel(canvasContext, conference) + override fun makeInitModel() = ConferenceDetailsModel(canvasContext, conference) - override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = - ConferenceDetailsView( - canvasContext, - inflater, - parent - ) + override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = ConferenceDetailsView( + canvasContext, + inflater, + parent + ) - @PageViewUrlParam("conferenceId") - fun getConferenceId() = conference.id - - companion object { - fun makeRoute(canvasContext: CanvasContext, conference: Conference): Route { - val bundle = canvasContext.makeBundle { - putParcelable(Const.CONFERENCE, conference) - } - return Route(null, ConferenceDetailsFragment::class.java, canvasContext, bundle) - } - - private fun validRoute(route: Route) = - route.canvasContext != null && route.arguments.containsKey(Const.CONFERENCE) - - fun newInstance(route: Route): ConferenceDetailsFragment? { - if (!validRoute(route)) return null - return ConferenceDetailsFragment() - .withArgs(route.arguments) - } - } + abstract fun getRepository(): ConferenceDetailsRepository } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsRepositoryFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsRepositoryFragment.kt new file mode 100644 index 0000000000..0e41e621ed --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsRepositoryFragment.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.conferences.conference_details.ui + +import android.os.Bundle +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.pageview.PageView +import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam +import com.instructure.interactions.router.Route +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.makeBundle +import com.instructure.pandautils.utils.withArgs +import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsRepository +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +@PageView(url = "{canvasContext}/conferences/{conferenceId}") +class ConferenceDetailsRepositoryFragment : ConferenceDetailsFragment() { + + @Inject + lateinit var conferenceDetailsRepository: ConferenceDetailsRepository + + override fun getRepository() = conferenceDetailsRepository + + @PageViewUrlParam("conferenceId") + fun getConferenceId() = conference.id + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = false + } + + companion object { + fun makeRoute(canvasContext: CanvasContext, conference: Conference): Route { + val bundle = canvasContext.makeBundle { + putParcelable(Const.CONFERENCE, conference) + } + return Route(null, ConferenceDetailsRepositoryFragment::class.java, canvasContext, bundle) + } + + private fun validRoute(route: Route) = + route.canvasContext != null && route.arguments.containsKey(Const.CONFERENCE) + + fun newInstance(route: Route): ConferenceDetailsRepositoryFragment? { + if (!validRoute(route)) return null + return ConferenceDetailsRepositoryFragment().withArgs(route.arguments) + } + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt index 5e1b3de45d..f99aeedbae 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_details/ui/ConferenceDetailsView.kt @@ -52,7 +52,7 @@ class ConferenceDetailsView(val canvasContext: CanvasContext, inflater: LayoutIn override fun onConnect(output: Consumer) { binding.swipeRefreshLayout.setOnRefreshListener { output.accept(ConferenceDetailsEvent.PullToRefresh) } - binding.joinButton.onClick { output.accept(ConferenceDetailsEvent.JoinConferenceClicked) } + binding.joinButton.onClickWithRequireNetwork { output.accept(ConferenceDetailsEvent.JoinConferenceClicked) } } override fun onDispose() = Unit @@ -86,7 +86,7 @@ class ConferenceDetailsView(val canvasContext: CanvasContext, inflater: LayoutIn private fun makeRecordingListItem(state: ConferenceRecordingViewState): View { val binding = AdapterConferenceRecordingItemBinding.inflate(LayoutInflater.from(context), null, false) with(binding) { - binding.root.onClick { consumer?.accept(ConferenceDetailsEvent.RecordingClicked(state.recordingId)) } + binding.root.onClickWithRequireNetwork { consumer?.accept(ConferenceDetailsEvent.RecordingClicked(state.recordingId)) } recordingTitle.text = state.title recordingDate.text = state.date recordingDuration.text = state.duration diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListEffectHandler.kt index 9503f3d5ed..3c94ba4adf 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListEffectHandler.kt @@ -16,17 +16,15 @@ */ package com.instructure.student.mobius.conferences.conference_list -import com.instructure.canvasapi2.managers.ConferenceManager -import com.instructure.canvasapi2.managers.OAuthManager -import com.instructure.canvasapi2.models.AuthenticatedSession import com.instructure.canvasapi2.utils.exhaustive -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.student.mobius.common.ui.EffectHandler import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListView import kotlinx.coroutines.delay import kotlinx.coroutines.launch -class ConferenceListEffectHandler : EffectHandler() { +class ConferenceListEffectHandler( + private val repository: ConferenceListRepository +) : EffectHandler() { override fun accept(effect: ConferenceListEffect) { when (effect) { is ConferenceListEffect.LoadData -> loadData(effect) @@ -37,9 +35,7 @@ class ConferenceListEffectHandler : EffectHandler { OAuthManager.getAuthenticatedSession(url, it) } - authenticatedUrl = authSession.sessionUrl + authenticatedUrl = repository.getAuthenticatedSession(url).sessionUrl } catch (e: Throwable) { // Try launching without authenticated URL } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListRepository.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListRepository.kt new file mode 100644 index 0000000000..2dc3ef7987 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ConferenceListRepository.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_list + +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListDataSource +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListLocalDataSource +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListNetworkDataSource + +class ConferenceListRepository( + localDataSource: ConferenceListLocalDataSource, + private val networkDataSource: ConferenceListNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getConferencesForContext( + canvasContext: CanvasContext, forceNetwork: Boolean + ): DataResult> { + return dataSource().getConferencesForContext(canvasContext, forceNetwork) + } + + suspend fun getAuthenticatedSession(targetUrl: String): AuthenticatedSession { + return networkDataSource.getAuthenticatedSession(targetUrl) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListDataSource.kt new file mode 100644 index 0000000000..0e1e5dbaa8 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListDataSource.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult + +interface ConferenceListDataSource { + + suspend fun getConferencesForContext(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListLocalDataSource.kt new file mode 100644 index 0000000000..3704766fb5 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListLocalDataSource.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.ConferenceFacade + +class ConferenceListLocalDataSource( + private val conferenceFacade: ConferenceFacade +) : ConferenceListDataSource { + + override suspend fun getConferencesForContext(canvasContext: CanvasContext, forceNetwork: Boolean): DataResult> { + return DataResult.Success(conferenceFacade.getConferencesByCourseId(canvasContext.id)) + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListNetworkDataSource.kt new file mode 100644 index 0000000000..8c39c15d33 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/datasource/ConferenceListNetworkDataSource.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.mobius.conferences.conference_list.datasource + +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class ConferenceListNetworkDataSource( + private val conferencesApi: ConferencesApi.ConferencesInterface, + private val oAuthApi: OAuthAPI.OAuthInterface +) : ConferenceListDataSource { + + override suspend fun getConferencesForContext( + canvasContext: CanvasContext, forceNetwork: Boolean + ): DataResult> { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + + return conferencesApi.getConferencesForContext(canvasContext.toAPIString().drop(1), params).map { + it.conferences + }.depaginate { url -> + conferencesApi.getNextPage(url, params).map { it.conferences } + } + } + + suspend fun getAuthenticatedSession(targetUrl: String): AuthenticatedSession { + val params = RestParams(isForceReadFromNetwork = true) + + return oAuthApi.getAuthenticatedSession(targetUrl, params).dataOrThrow + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListFragment.kt index a02fbc4789..cfefa5e6c0 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListFragment.kt @@ -20,57 +20,34 @@ import android.view.LayoutInflater import android.view.ViewGroup import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_CONFERENCE_LIST import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.makeBundle -import com.instructure.pandautils.utils.withArgs import com.instructure.student.databinding.FragmentConferenceListBinding import com.instructure.student.mobius.common.ui.MobiusFragment import com.instructure.student.mobius.conferences.conference_list.* @PageView(url = "{canvasContext}/conferences") @ScreenView(SCREEN_VIEW_CONFERENCE_LIST) -class ConferenceListFragment : - MobiusFragment() { +abstract class ConferenceListFragment : MobiusFragment() { val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - override fun makeUpdate() = - ConferenceListUpdate() + override fun makeUpdate() = ConferenceListUpdate() - override fun makePresenter() = - ConferenceListPresenter + override fun makePresenter() = ConferenceListPresenter - override fun makeEffectHandler() = - ConferenceListEffectHandler() + override fun makeEffectHandler() = ConferenceListEffectHandler(getRepository()) - override fun makeInitModel() = - ConferenceListModel(canvasContext) + override fun makeInitModel() = ConferenceListModel(canvasContext) - override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = - ConferenceListView( - canvasContext, - inflater, - parent - ) + override fun makeView(inflater: LayoutInflater, parent: ViewGroup) = ConferenceListView( + canvasContext, + inflater, + parent + ) - companion object { - fun makeRoute(canvasContext: CanvasContext): Route { - return Route(null, ConferenceListFragment::class.java, canvasContext, canvasContext.makeBundle()) - } - - private fun validRoute(route: Route) = route.canvasContext != null - - fun newInstance(route: Route): ConferenceListFragment? { - if (!validRoute( - route - ) - ) return null - return ConferenceListFragment() - .withArgs(route.arguments) - } - } + abstract fun getRepository(): ConferenceListRepository } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListRepositoryFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListRepositoryFragment.kt new file mode 100644 index 0000000000..ce1ba24c32 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListRepositoryFragment.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.mobius.conferences.conference_list.ui + +import android.os.Bundle +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.interactions.router.Route +import com.instructure.pandautils.utils.makeBundle +import com.instructure.pandautils.utils.withArgs +import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class ConferenceListRepositoryFragment : ConferenceListFragment() { + + @Inject + lateinit var conferenceListRepository: ConferenceListRepository + + override fun getRepository() = conferenceListRepository + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = false + } + + companion object { + fun makeRoute(canvasContext: CanvasContext): Route { + return Route(null, ConferenceListRepositoryFragment::class.java, canvasContext, canvasContext.makeBundle()) + } + + private fun validRoute(route: Route) = route.canvasContext != null + + fun newInstance(route: Route): ConferenceListRepositoryFragment? { + if (!validRoute(route)) return null + return ConferenceListRepositoryFragment().withArgs(route.arguments) + } + } +} diff --git a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt index a6649d1562..cb1fae604c 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/conferences/conference_list/ui/ConferenceListView.kt @@ -23,6 +23,7 @@ import android.view.MenuItem import android.view.ViewGroup import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference @@ -31,29 +32,35 @@ import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.databinding.FragmentConferenceListBinding import com.instructure.student.mobius.common.ui.MobiusView -import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsFragment +import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsRepositoryFragment import com.instructure.student.mobius.conferences.conference_list.ConferenceListEvent import com.instructure.student.router.RouteMatcher import com.spotify.mobius.functions.Consumer -class ConferenceListView(val canvasContext: CanvasContext, inflater: LayoutInflater, parent: ViewGroup) : - MobiusView( - inflater, - FragmentConferenceListBinding::inflate, - parent) { +class ConferenceListView( + val canvasContext: CanvasContext, + inflater: LayoutInflater, + parent: ViewGroup +) : MobiusView( + inflater, + FragmentConferenceListBinding::inflate, + parent +) { - lateinit var listAdapter: ConferenceListAdapter + private lateinit var listAdapter: ConferenceListAdapter init { binding.toolbar.setupAsBackButton { (context as? Activity)?.onBackPressed() } binding.toolbar.subtitle = canvasContext.name // Set up menu - with(binding.toolbar.menu.add(0, R.id.openExternallyButton, 0, R.string.openInBrowser)){ + with(binding.toolbar.menu.add(0, R.id.openExternallyButton, 0, R.string.openInBrowser)) { setIcon(R.drawable.ic_open_in_browser) setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) setOnMenuItemClickListener { - consumer?.accept(ConferenceListEvent.LaunchInBrowser) + (context as? FragmentActivity)?.withRequireNetwork { + consumer?.accept(ConferenceListEvent.LaunchInBrowser) + } true } } @@ -65,21 +72,13 @@ class ConferenceListView(val canvasContext: CanvasContext, inflater: LayoutInfla override fun onConnect(output: Consumer) { binding.swipeRefreshLayout.setOnRefreshListener { output.accept(ConferenceListEvent.PullToRefresh) } - listAdapter = - ConferenceListAdapter( - object : - ConferenceListAdapterCallback { - override fun onConferenceClicked(conferenceId: Long) { - output.accept( - ConferenceListEvent.ConferenceClicked( - conferenceId - ) - ) - } - - override fun reload() = - output.accept(ConferenceListEvent.PullToRefresh) - }) + listAdapter = ConferenceListAdapter(object : ConferenceListAdapterCallback { + override fun onConferenceClicked(conferenceId: Long) { + output.accept(ConferenceListEvent.ConferenceClicked(conferenceId)) + } + + override fun reload() = output.accept(ConferenceListEvent.PullToRefresh) + }) binding.recyclerView.layoutManager = LinearLayoutManager(context) binding.recyclerView.adapter = listAdapter } @@ -101,7 +100,7 @@ class ConferenceListView(val canvasContext: CanvasContext, inflater: LayoutInfla } fun showConferenceDetails(conference: Conference) { - RouteMatcher.route(context, ConferenceDetailsFragment.makeRoute(canvasContext, conference)) + RouteMatcher.route(activity as FragmentActivity, ConferenceDetailsRepositoryFragment.makeRoute(canvasContext, conference)) } fun launchUrl(url: String) { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt index b54f4cb54a..d09bd54d59 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/grades/StudentGradesRouter.kt @@ -21,7 +21,6 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Tab import com.instructure.pandautils.features.elementary.grades.GradesRouter import com.instructure.student.features.elementary.course.ElementaryCourseFragment -import com.instructure.student.fragment.GradesListFragment import com.instructure.student.router.RouteMatcher class StudentGradesRouter(private val activity: FragmentActivity) : GradesRouter { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt index cd90011cc7..ceab631c85 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/homeroom/StudentHomeroomRouter.kt @@ -22,10 +22,10 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.elementary.homeroom.HomeroomRouter -import com.instructure.student.BuildConfig +import com.instructure.student.features.assignments.list.AssignmentListFragment import com.instructure.student.features.elementary.course.ElementaryCourseFragment import com.instructure.student.flutterChannels.FlutterComm -import com.instructure.student.fragment.* +import com.instructure.student.fragment.AnnouncementListFragment import com.instructure.student.router.RouteMatcher class StudentHomeroomRouter(private val activity: FragmentActivity) : HomeroomRouter { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt index 112b8b9cf5..dfd4a01fbb 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/importantdates/StudentImportantDatesRouter.kt @@ -20,7 +20,7 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.pandautils.features.elementary.importantdates.ImportantDatesRouter -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.fragment.CalendarEventFragment import com.instructure.student.router.RouteMatcher diff --git a/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt index 9d7b91d813..58f0935893 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/elementary/schedule/StudentScheduleRouter.kt @@ -22,7 +22,7 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.elementary.schedule.ScheduleRouter -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.features.elementary.course.ElementaryCourseFragment import com.instructure.student.fragment.BasicQuizViewFragment import com.instructure.student.fragment.CalendarEventFragment diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt index 49e948fc9f..837b9dd728 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusEffectHandler.kt @@ -26,7 +26,7 @@ import com.instructure.student.mobius.common.ui.EffectHandler import com.instructure.student.mobius.syllabus.ui.SyllabusView import kotlinx.coroutines.launch -class SyllabusEffectHandler : EffectHandler() { +class SyllabusEffectHandler(private val repository: SyllabusRepository) : EffectHandler() { override fun accept(effect: SyllabusEffect) { when (effect) { is SyllabusEffect.LoadData -> loadData(effect) @@ -37,12 +37,10 @@ class SyllabusEffectHandler : EffectHandler> if (course.isFail) { @@ -56,11 +54,8 @@ class SyllabusEffectHandler : EffectHandler() assignments.map { endList.addAll(it) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt new file mode 100644 index 0000000000..0b65dfd7ad --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/SyllabusRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.mobius.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.repository.Repository +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.syllabus.datasource.SyllabusDataSource +import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource +import com.instructure.student.mobius.syllabus.datasource.SyllabusNetworkDataSource + +class SyllabusRepository( + syllabusLocalDataSource: SyllabusLocalDataSource, + syllabusNetworkDataSource: SyllabusNetworkDataSource, + networkStateProvider: NetworkStateProvider, + featureFlagProvider: FeatureFlagProvider +) : Repository(syllabusLocalDataSource, syllabusNetworkDataSource, networkStateProvider, featureFlagProvider) { + + suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return dataSource().getCourseSettings(courseId, forceNetwork) + } + + suspend fun getCourseWithSyllabus(courseId: Long, forceNetwork: Boolean): DataResult { + return dataSource().getCourseWithSyllabus(courseId, forceNetwork) + } + + suspend fun getCalendarEvents( + allEvents: Boolean, + type: CalendarEventAPI.CalendarEventType, + startDate: String?, + endDate: String?, + canvasContexts: List, + forceNetwork: Boolean + ): DataResult> { + return dataSource().getCalendarEvents(allEvents, type, startDate, endDate, canvasContexts, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt new file mode 100644 index 0000000000..947c812ca9 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusDataSource.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.mobius.syllabus.datasource + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult + +interface SyllabusDataSource { + + suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? + + suspend fun getCourseWithSyllabus(courseId: Long, forceNetwork: Boolean): DataResult + + suspend fun getCalendarEvents( + allEvents: Boolean, + type: CalendarEventAPI.CalendarEventType, + startDate: String?, + endDate: String?, + canvasContexts: List, + forceNetwork: Boolean + ): DataResult> +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt new file mode 100644 index 0000000000..ec16f6e37b --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusLocalDataSource.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.mobius.syllabus.datasource + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade + +class SyllabusLocalDataSource( + private val courseSettingsDao: CourseSettingsDao, + private val courseFacade: CourseFacade, + private val scheduleItemFacade: ScheduleItemFacade +) : SyllabusDataSource { + + override suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + return courseSettingsDao.findByCourseId(courseId)?.toApiModel() + } + + override suspend fun getCourseWithSyllabus(courseId: Long, forceNetwork: Boolean): DataResult { + return courseFacade.getCourseById(courseId)?.let { + DataResult.Success(it) + } ?: DataResult.Fail() + } + + override suspend fun getCalendarEvents( + allEvents: Boolean, + type: CalendarEventAPI.CalendarEventType, + startDate: String?, + endDate: String?, + canvasContexts: List, + forceNetwork: Boolean + ): DataResult> { + return try { + DataResult.Success(scheduleItemFacade.findByItemType(canvasContexts, type.apiName)) + } catch (e: Exception) { + DataResult.Fail() + } + + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt new file mode 100644 index 0000000000..a237849f11 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/datasource/SyllabusNetworkDataSource.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.mobius.syllabus.datasource + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate + +class SyllabusNetworkDataSource( + private val courseApi: CourseAPI.CoursesInterface, + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface +) : SyllabusDataSource { + + override suspend fun getCourseSettings(courseId: Long, forceNetwork: Boolean): CourseSettings? { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return courseApi.getCourseSettings(courseId, restParams).dataOrNull + } + + override suspend fun getCourseWithSyllabus(courseId: Long, forceNetwork: Boolean): DataResult { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return courseApi.getCourseWithSyllabus(courseId, restParams) + } + + override suspend fun getCalendarEvents( + allEvents: Boolean, + type: CalendarEventAPI.CalendarEventType, + startDate: String?, + endDate: String?, + canvasContexts: List, + forceNetwork: Boolean + ): DataResult> { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + return calendarEventApi.getCalendarEvents( + allEvents, + type.apiName, + startDate, + endDate, + canvasContexts, + restParams + ).depaginate { calendarEventApi.next(it, restParams) } + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt index 8b7f01ca91..b0b37257e4 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusFragment.kt @@ -20,24 +20,21 @@ import android.view.LayoutInflater import android.view.ViewGroup import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_SYLLABUS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.makeBundle -import com.instructure.pandautils.utils.withArgs import com.instructure.student.databinding.FragmentSyllabusBinding import com.instructure.student.mobius.common.ui.MobiusFragment import com.instructure.student.mobius.syllabus.* @ScreenView(SCREEN_VIEW_SYLLABUS) @PageView(url = "{canvasContext}/assignments/syllabus") -class SyllabusFragment : MobiusFragment() { +abstract class SyllabusFragment : MobiusFragment() { val canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - override fun makeEffectHandler() = SyllabusEffectHandler() + override fun makeEffectHandler() = SyllabusEffectHandler(getRepository()) override fun makeUpdate() = SyllabusUpdate() @@ -47,20 +44,5 @@ class SyllabusFragment : MobiusFragment) : PagerAdapter() { +class SyllabusTabAdapter(private val context: Context, private val canvasContext: CanvasContext, private val titles: List) : PagerAdapter() { var eventsBinding: FragmentSyllabusEventsBinding? = null var webviewBinding: FragmentSyllabusWebviewBinding? = null @@ -76,7 +77,7 @@ class SyllabusTabAdapter(private val canvasContext: CanvasContext, private val t private fun isSyllabusPosition(position: Int) = position == 0 private fun setupWebView(webView: CanvasWebView) { - val activity = (webView.context as? FragmentActivity) + val activity = (context as? FragmentActivity) activity?.let { webView.addVideoClient(it) } webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun openMediaFromWebView(mime: String, url: String, filename: String) { @@ -87,26 +88,24 @@ class SyllabusTabAdapter(private val canvasContext: CanvasContext, private val t override fun onPageFinishedCallback(webView: WebView, url: String) {} override fun canRouteInternallyDelegate(url: String): Boolean { - return RouteMatcher.canRouteInternally(webView.context, url, ApiPrefs.domain, false) + return RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, false) } override fun routeInternallyCallback(url: String) { - RouteMatcher.canRouteInternally(webView.context, url, ApiPrefs.domain, true) + RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, true) } } - webView.canvasEmbeddedWebViewCallback = - object : CanvasWebView.CanvasEmbeddedWebViewCallback { - override fun shouldLaunchInternalWebViewFragment(url: String): Boolean { - return true - } - - override fun launchInternalWebViewFragment(url: String) { - InternalWebviewFragment.loadInternalWebView( - webView.context, - InternalWebviewFragment.makeRoute(canvasContext, url, false) - ) - } + webView.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { + override fun shouldLaunchInternalWebViewFragment(url: String): Boolean { + return true } + override fun launchInternalWebViewFragment(url: String) { + InternalWebviewFragment.loadInternalWebView( + activity, + InternalWebviewFragment.makeRoute(canvasContext, url, false) + ) + } + } } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt index 0d264ec66a..ca5b9614db 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/syllabus/ui/SyllabusView.kt @@ -19,6 +19,7 @@ package com.instructure.student.mobius.syllabus.ui import android.app.Activity import android.view.LayoutInflater import android.view.ViewGroup +import androidx.fragment.app.FragmentActivity import com.google.android.material.tabs.TabLayout import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext @@ -29,7 +30,7 @@ import com.instructure.student.R import com.instructure.student.databinding.FragmentSyllabusBinding import com.instructure.student.databinding.FragmentSyllabusEventsBinding import com.instructure.student.databinding.FragmentSyllabusWebviewBinding -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.fragment.CalendarEventFragment import com.instructure.student.mobius.common.ui.MobiusView import com.instructure.student.mobius.syllabus.SyllabusEvent @@ -58,11 +59,11 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p } init { - binding.toolbar.setupAsBackButton { (context as? Activity)?.onBackPressed() } + binding.toolbar.setupAsBackButton { activity.onBackPressed() } binding.toolbar.title = context.getString(com.instructure.pandares.R.string.syllabus) binding.toolbar.subtitle = canvasContext.name - adapter = SyllabusTabAdapter(canvasContext, getTabTitles()) + adapter = SyllabusTabAdapter(activity, canvasContext, getTabTitles()) binding.syllabusPager.adapter = adapter binding.syllabusTabLayout.setupWithViewPager(binding.syllabusPager, true) @@ -142,11 +143,11 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p } fun showAssignmentView(assignment: Assignment, canvasContext: CanvasContext) { - RouteMatcher.route(context, AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) + RouteMatcher.route(activity as FragmentActivity, AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) } fun showScheduleItemView(scheduleItem: ScheduleItem, canvasContext: CanvasContext) { - RouteMatcher.route(context, CalendarEventFragment.makeRoute(canvasContext, scheduleItem)) + RouteMatcher.route(activity as FragmentActivity, CalendarEventFragment.makeRoute(canvasContext, scheduleItem)) } private fun getTabTitles(): List = listOf( diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt index cb24098b0e..5812be3da6 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteMatcher.kt @@ -16,7 +16,6 @@ */ package com.instructure.student.router -import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.os.Bundle @@ -24,38 +23,53 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import androidx.loader.app.LoaderManager import androidx.loader.content.Loader -import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.managers.FileFolderManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.FileFolder import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.ApiType -import com.instructure.canvasapi2.utils.LinkHeaders import com.instructure.canvasapi2.utils.Logger +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.interactions.router.* import com.instructure.pandautils.activities.BaseViewMediaActivity import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.pandautils.features.offline.sync.progress.SyncProgressFragment import com.instructure.pandautils.features.shareextension.ShareFileSubmissionTarget import com.instructure.pandautils.loaders.OpenMediaAsyncTaskLoader +import com.instructure.pandautils.room.offline.OfflineDatabase import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.LoaderUtils +import com.instructure.pandautils.utils.NetworkStateProvider import com.instructure.pandautils.utils.RouteUtils import com.instructure.pandautils.utils.nonNullArgs import com.instructure.student.R import com.instructure.student.activity.* -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.assignments.list.AssignmentListFragment +import com.instructure.student.features.coursebrowser.CourseBrowserFragment +import com.instructure.student.features.discussion.details.DiscussionDetailsFragment +import com.instructure.student.features.discussion.list.DiscussionListFragment import com.instructure.student.features.elementary.course.ElementaryCourseFragment +import com.instructure.student.features.files.list.FileListFragment +import com.instructure.student.features.grades.GradesListFragment +import com.instructure.student.features.modules.list.ModuleListFragment +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment +import com.instructure.student.features.pages.details.PageDetailsFragment +import com.instructure.student.features.pages.list.PageListFragment +import com.instructure.student.features.people.details.PeopleDetailsFragment +import com.instructure.student.features.people.list.PeopleListFragment +import com.instructure.student.features.quiz.list.QuizListFragment import com.instructure.student.fragment.* import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment -import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment -import com.instructure.student.mobius.syllabus.ui.SyllabusFragment +import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment +import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment import com.instructure.student.util.FileUtils -import retrofit2.Call -import retrofit2.Response +import com.instructure.student.util.onMainThread import java.util.* import java.util.regex.Pattern @@ -64,12 +78,16 @@ object RouteMatcher : BaseRouteMatcher() { private var openMediaBundle: Bundle? = null private var openMediaCallbacks: LoaderManager.LoaderCallbacks? = null // I'll bet this causes a memory leak + var offlineDb: OfflineDatabase? = null + var networkStateProvider: NetworkStateProvider? = null + init { initRoutes() initClassMap() } // Be sensitive to the order of items. It really, really matters. + @androidx.annotation.OptIn(com.google.android.material.badge.ExperimentalBadgeUtils::class) private fun initRoutes() { routes.add(Route("/", DashboardFragment::class.java)) // region Conversations @@ -82,7 +100,14 @@ object RouteMatcher : BaseRouteMatcher() { // Courses ////////////////////////// routes.add(Route(courseOrGroup("/"), DashboardFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}"), CourseBrowserFragment::class.java, NotificationListFragment::class.java, Arrays.asList(":${RouterParams.RECENT_ACTIVITY}"))) // Recent Activity + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}"), + CourseBrowserFragment::class.java, + NotificationListFragment::class.java, + listOf(":${RouterParams.RECENT_ACTIVITY}") + ) + ) // Recent Activity if (ApiPrefs.showElementaryView) { routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}"), ElementaryCourseFragment::class.java)) } else { @@ -102,14 +127,55 @@ object RouteMatcher : BaseRouteMatcher() { } else { routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/modules"), ModuleListFragment::class.java)) } - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/modules/items/:${RouterParams.MODULE_ITEM_ID}"), ModuleListFragment::class.java, CourseModuleProgressionFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/modules/items/:${RouterParams.MODULE_ITEM_ID}"), + ModuleListFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/modules/:${RouterParams.MODULE_ID}"), ModuleListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/pages/:${RouterParams.PAGE_ID}"), ModuleListFragment::class.java, CourseModuleProgressionFragment::class.java, listOf(":${RouterParams.MODULE_ITEM_ID}"))) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes/:${RouterParams.QUIZ_ID}"), ModuleListFragment::class.java, CourseModuleProgressionFragment::class.java, listOf(":${RouterParams.MODULE_ITEM_ID}"))) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), ModuleListFragment::class.java, CourseModuleProgressionFragment::class.java, listOf(":${RouterParams.MODULE_ITEM_ID}"))) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), ModuleListFragment::class.java, CourseModuleProgressionFragment::class.java, listOf(":${RouterParams.MODULE_ITEM_ID}"))) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}"), ModuleListFragment::class.java, CourseModuleProgressionFragment::class.java, listOf(":${RouterParams.MODULE_ITEM_ID}"))) // TODO TEST + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/pages/:${RouterParams.PAGE_ID}"), + ModuleListFragment::class.java, + CourseModuleProgressionFragment::class.java, + listOf(":${RouterParams.MODULE_ITEM_ID}") + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes/:${RouterParams.QUIZ_ID}"), + ModuleListFragment::class.java, + CourseModuleProgressionFragment::class.java, + listOf(":${RouterParams.MODULE_ITEM_ID}") + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), + ModuleListFragment::class.java, + CourseModuleProgressionFragment::class.java, + listOf(":${RouterParams.MODULE_ITEM_ID}") + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + ModuleListFragment::class.java, + CourseModuleProgressionFragment::class.java, + listOf(":${RouterParams.MODULE_ITEM_ID}") + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}"), + ModuleListFragment::class.java, + CourseModuleProgressionFragment::class.java, + listOf(":${RouterParams.MODULE_ITEM_ID}") + ) + ) // TODO TEST // endregion // Notifications @@ -121,71 +187,199 @@ object RouteMatcher : BaseRouteMatcher() { } else { routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/grades"), GradesListFragment::class.java)) } - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/grades/:${RouterParams.ASSIGNMENT_ID}"), GradesListFragment::class.java, AssignmentDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/grades/:${RouterParams.ASSIGNMENT_ID}"), + GradesListFragment::class.java, + AssignmentDetailsFragment::class.java + ) + ) // People routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/users"), PeopleListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/users/:${RouterParams.USER_ID}"), PeopleListFragment::class.java, PeopleDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/users/:${RouterParams.USER_ID}"), + PeopleListFragment::class.java, + PeopleDetailsFragment::class.java + ) + ) // Files routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files"), FileListFragment::class.java)) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files/folder/:${RouterParams.FOLDER_NAME}"), RouteContext.FILE)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}/download"), RouteContext.FILE)) // trigger webview's download listener + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}/download"), + RouteContext.FILE + ) + ) // trigger webview's download listener routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}/preview"), RouteContext.FILE)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}"), RouteContext.FILE, CourseModuleProgressionFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/files/:${RouterParams.FILE_ID}"), + RouteContext.FILE, + CourseModuleProgressionFragment::class.java + ) + ) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/files/folder(\\/.*)*/:${RouterParams.FILE_ID}"), RouteContext.FILE)) routes.add(Route("/files/folder(\\/.*)*/:${RouterParams.FILE_ID}", RouteContext.FILE)) // Discussions routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics"), DiscussionListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), DiscussionListFragment::class.java, CourseModuleProgressionFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), DiscussionListFragment::class.java, DiscussionRouterFragment::class.java)) // Route for bookmarking + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), + DiscussionListFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/discussion_topics/:${RouterParams.MESSAGE_ID}"), + DiscussionListFragment::class.java, + DiscussionRouterFragment::class.java + ) + ) // Route for bookmarking // Pages routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/pages"), PageListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/pages/:${RouterParams.PAGE_ID}"), PageListFragment::class.java, CourseModuleProgressionFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/pages/:${RouterParams.PAGE_ID}"), PageListFragment::class.java, PageDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/pages/:${RouterParams.PAGE_ID}"), + PageListFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/pages/:${RouterParams.PAGE_ID}"), + PageListFragment::class.java, + PageDetailsFragment::class.java + ) + ) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/wiki"), PageListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/wiki/:${RouterParams.PAGE_ID}"), PageListFragment::class.java, PageDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/wiki/:${RouterParams.PAGE_ID}"), + PageListFragment::class.java, + PageDetailsFragment::class.java + ) + ) // Announcements routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/announcements"), AnnouncementListFragment::class.java)) // :message_id because it shares with discussions - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/announcements/:${RouterParams.MESSAGE_ID}"), AnnouncementListFragment::class.java, DiscussionRouterFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/announcements/:${RouterParams.MESSAGE_ID}"), + AnnouncementListFragment::class.java, + DiscussionRouterFragment::class.java + ) + ) // Announcements from the notifications tab - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/announcements/:${RouterParams.MESSAGE_ID}"), NotificationListFragment::class.java, DiscussionRouterFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/announcements/:${RouterParams.MESSAGE_ID}"), + NotificationListFragment::class.java, + DiscussionRouterFragment::class.java + ) + ) // Quiz routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes"), QuizListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes/:${RouterParams.QUIZ_ID}"), QuizListFragment::class.java, CourseModuleProgressionFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes/:${RouterParams.QUIZ_ID}"), QuizListFragment::class.java, BasicQuizViewFragment::class.java)) - + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes/:${RouterParams.QUIZ_ID}"), + QuizListFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/quizzes/:${RouterParams.QUIZ_ID}"), + QuizListFragment::class.java, + BasicQuizViewFragment::class.java + ) + ) // Calendar routes.add(Route("/calendar", CalendarFragment::class.java)) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/calendar_events/:${RouterParams.EVENT_ID}"), CalendarFragment::class.java)) // Syllabus - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/syllabus"), SyllabusFragment::class.java)) + routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/syllabus"), SyllabusRepositoryFragment::class.java)) // Assignments routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments"), AssignmentListFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), AssignmentListFragment::class.java, CourseModuleProgressionFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), NotificationListFragment::class.java, CourseModuleProgressionFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), CalendarFragment::class.java, CourseModuleProgressionFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), AssignmentListFragment::class.java, AssignmentDetailsFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), NotificationListFragment::class.java, AssignmentDetailsFragment::class.java)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), CalendarFragment::class.java, AssignmentDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + AssignmentListFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + NotificationListFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + CalendarFragment::class.java, + CourseModuleProgressionFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + AssignmentListFragment::class.java, + AssignmentDetailsFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + NotificationListFragment::class.java, + AssignmentDetailsFragment::class.java + ) + ) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + CalendarFragment::class.java, + AssignmentDetailsFragment::class.java + ) + ) // Studio - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/external_tools/:${RouterParams.EXTERNAL_ID}/resource_selection"), StudioWebViewFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/external_tools/:${RouterParams.EXTERNAL_ID}/resource_selection"), + StudioWebViewFragment::class.java + ) + ) // Submissions // :sliding_tab_type can be /rubric or /submissions (used to navigate to the nested fragment) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}/:${RouterParams.SLIDING_TAB_TYPE}"), AssignmentDetailsFragment::class.java, SubmissionDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}/:${RouterParams.SLIDING_TAB_TYPE}"), + AssignmentDetailsFragment::class.java, + SubmissionDetailsFragment::class.java + ) + ) // Route to Assignment Details first - no submission/on paper assignments won't have grades on the Submission Details page, but we also need to account for routing to submission comments (Assignment Details will check for that) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}/:${RouterParams.SLIDING_TAB_TYPE}/:${RouterParams.SUBMISSION_ID}"), AssignmentDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}/:${RouterParams.SLIDING_TAB_TYPE}/:${RouterParams.SUBMISSION_ID}"), + AssignmentDetailsFragment::class.java + ) + ) // Settings routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/settings"), CourseSettingsFragment::class.java)) @@ -196,12 +390,26 @@ object RouteMatcher : BaseRouteMatcher() { // Unsupported // NOTE: An Exception to how the router usually works (Not recommended for urls that are meant to be internally routed) // The .* will catch anything and route to UnsupportedFragment. If the users decides to press "open in browser" from the UnsupportedFragment, then InternalWebviewFragment is setup to handle the unsupportedFeature - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/lti_collaborations.*"), UnsupportedTabFragment::class.java, Tab.COLLABORATIONS_ID)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/lti_collaborations.*"), + UnsupportedTabFragment::class.java, + Tab.COLLABORATIONS_ID + ) + ) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/collaborations.*"), UnsupportedTabFragment::class.java, Tab.COLLABORATIONS_ID)) routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/outcomes.*"), UnsupportedTabFragment::class.java, Tab.OUTCOMES_ID)) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/conferences.*"), ConferenceListFragment::class.java, Tab.CONFERENCES_ID)) - - routes.add(Route("/files", FileListFragment::class.java).apply{ canvasContext = ApiPrefs.user }) // validRoute for FileListFragment checks for a canvasContext, which is null on deep links + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/conferences.*"), + ConferenceListRepositoryFragment::class.java, + Tab.CONFERENCES_ID + ) + ) + + routes.add(Route("/files", FileListFragment::class.java).apply { + canvasContext = ApiPrefs.user + }) // validRoute for FileListFragment checks for a canvasContext, which is null on deep links routes.add(Route("/files/folder/:${RouterParams.FOLDER_NAME}", RouteContext.FILE)) routes.add(Route("/files/:${RouterParams.FILE_ID}/download", RouteContext.FILE)) // trigger webview's download listener routes.add(Route("/files/:${RouterParams.FILE_ID}", RouteContext.FILE)) // Triggered by new RCE content file links @@ -213,13 +421,22 @@ object RouteMatcher : BaseRouteMatcher() { routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/external_tools/:${RouterParams.EXTERNAL_ID}"), RouteContext.LTI)) //Single Detail Pages (Typically routing from To-dos (may not be handling every use case) - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), AssignmentDetailsFragment::class.java, null)) + routes.add( + Route( + courseOrGroup("/:${RouterParams.COURSE_ID}/assignments/:${RouterParams.ASSIGNMENT_ID}"), + AssignmentDetailsFragment::class.java, + null + ) + ) routes.add(Route("/enroll/.*", RouteContext.DO_NOT_ROUTE)) + routes.add(Route("/syncProgress", SyncProgressFragment::class.java)) + // Catch all (when nothing has matched, these take over) // Note: Catch all only happens with supported domains such as instructure.com - routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/.*"), UnsupportedFeatureFragment::class.java)) // course_id fetches the course context + routes.add(Route(courseOrGroup("/:${RouterParams.COURSE_ID}/.*"), UnsupportedFeatureFragment::class.java)) + // course_id fetches the course context routes.add(Route(".*", UnsupportedFeatureFragment::class.java)) } @@ -231,11 +448,11 @@ object RouteMatcher : BaseRouteMatcher() { // bottomSheetFragments.add(EditFavoritesFragment::class.java) } - fun routeUrl(context: Context, url: String, extras: Bundle? = null) { - routeUrl(context, url, ApiPrefs.domain, extras) + fun routeUrl(activity: FragmentActivity, url: String, extras: Bundle? = null) { + routeUrl(activity, url, ApiPrefs.domain, extras) } - fun routeUrl(context: Context, url: String, domain: String, extras: Bundle? = null, secondaryClass: Class? = null) { + fun routeUrl(activity: FragmentActivity, url: String, domain: String, extras: Bundle? = null, secondaryClass: Class? = null) { /* Possible activity types we can navigate too: Unknown Link, InitActivity, Master/Detail, Fullscreen, WebView, ViewMedia */ //Find the best route @@ -245,9 +462,11 @@ object RouteMatcher : BaseRouteMatcher() { val route = getInternalRoute(url, domain) // Prevent routing to unsupported features while in student view - if (ApiPrefs.isStudentView && - (route?.primaryClass == InboxFragment::class.java || - route?.tabId == Tab.CONFERENCES_ID || route?.tabId == Tab.COLLABORATIONS_ID)) { + if (ApiPrefs.isStudentView + && (route?.primaryClass == InboxFragment::class.java + || route?.tabId == Tab.CONFERENCES_ID + || route?.tabId == Tab.COLLABORATIONS_ID) + ) { route.primaryClass = NothingToSeeHereFragment::class.java } @@ -255,7 +474,7 @@ object RouteMatcher : BaseRouteMatcher() { // The Group API will not load an individual user's details, so we route to the List fragment by default // FIXME: Remove if the group context works with grabbing a user - if (route?.getContextType() == CanvasContext.Type.GROUP && route.primaryClass == PeopleListFragment::class.java && route.secondaryClass == PeopleDetailsFragment::class.java ) { + if (route?.getContextType() == CanvasContext.Type.GROUP && route.primaryClass == PeopleListFragment::class.java && route.secondaryClass == PeopleDetailsFragment::class.java) { route.primaryClass = null route.secondaryClass = PeopleListFragment::class.java } @@ -264,29 +483,28 @@ object RouteMatcher : BaseRouteMatcher() { route?.secondaryClass = secondaryClass } - route(context, route) + route(activity, route) } - - fun route(context: Context, route: Route?) { - + fun route(activity: FragmentActivity, route: Route?) { if (route == null || route.routeContext == RouteContext.DO_NOT_ROUTE) { if (route?.uri != null) { // No route, no problem - handleWebViewUrl(context, route.uri.toString()) + handleWebViewUrl(activity, route.uri.toString()) } - } else if (route.routeContext == RouteContext.FILE || route.primaryClass?.isAssignableFrom(FileListFragment::class.java) == true && route.queryParamsHash.containsKey(RouterParams.PREVIEW)) { + } else if (route.routeContext == RouteContext.FILE + || route.primaryClass?.isAssignableFrom(FileListFragment::class.java) == true + && route.queryParamsHash.containsKey(RouterParams.PREVIEW) + ) { when { - route.secondaryClass == CourseModuleProgressionFragment::class.java -> handleFullscreenRoute(context, route) + route.secondaryClass == CourseModuleProgressionFragment::class.java -> handleFullscreenRoute(activity, route) route.queryParamsHash.containsKey(RouterParams.VERIFIER) && route.queryParamsHash.containsKey(RouterParams.DOWNLOAD_FRD) -> { if (route.removePreviousScreen) { - val fragmentManager = (context as? FragmentActivity)?.supportFragmentManager - fragmentManager?.popBackStackImmediate() + val fragmentManager = activity.supportFragmentManager + fragmentManager.popBackStackImmediate() } if (route.uri != null) { - openMedia(context as FragmentActivity, route.uri.toString()) - } else if (route.uri != null) { - openMedia(context as FragmentActivity, route.uri!!.toString()) + openMedia(activity, route.uri.toString()) } } route.paramsHash.containsKey(RouterParams.FOLDER_NAME) && !route.queryParamsHash.containsKey(RouterParams.PREVIEW) -> { @@ -297,30 +515,32 @@ object RouteMatcher : BaseRouteMatcher() { } route.routeContext = RouteContext.UNKNOWN route.primaryClass = FileListFragment::class.java - handleFullscreenRoute(context, route) + handleFullscreenRoute(activity, route) } else -> { if (route.removePreviousScreen) { - val fragmentManager = (context as? FragmentActivity)?.supportFragmentManager - fragmentManager?.popBackStackImmediate() + val fragmentManager = activity.supportFragmentManager + fragmentManager.popBackStackImmediate() } val isGroupRoute = "groups" == route.uri?.pathSegments?.get(0) handleSpecificFile( - context as FragmentActivity, - (if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) route.queryParamsHash[RouterParams.PREVIEW] else route.paramsHash[RouterParams.FILE_ID]) ?: "", + activity, + (if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) route.queryParamsHash[RouterParams.PREVIEW] else route.paramsHash[RouterParams.FILE_ID]) + ?: "", route, - isGroupRoute) + isGroupRoute + ) } } } else if (route.routeContext == RouteContext.MEDIA) { - handleMediaRoute(context, route) + handleMediaRoute(activity, route) } else if (route.routeContext == RouteContext.SPEED_GRADER) { //handleSpeedGraderRoute(context, route) //Annotations for student maybe? - } else if (context.resources.getBoolean(R.bool.isDeviceTablet)) { - handleTabletRoute(context, route) + } else if (activity.resources.getBoolean(R.bool.isDeviceTablet)) { + handleTabletRoute(activity, route) } else { - handleFullscreenRoute(context, route) - (context as? InterwebsToApplication)?.finish() + handleFullscreenRoute(activity, route) + (activity as? InterwebsToApplication)?.finish() } } @@ -347,17 +567,17 @@ object RouteMatcher : BaseRouteMatcher() { Logger.i("RouteMatcher:handleWebViewRoute()") } - private fun getLoaderCallbacks(activity: Activity): LoaderManager.LoaderCallbacks { + private fun getLoaderCallbacks(activity: FragmentActivity): LoaderManager.LoaderCallbacks { if (openMediaCallbacks == null) { openMediaCallbacks = object : LoaderManager.LoaderCallbacks { var dialog: AlertDialog? = null override fun onCreateLoader(id: Int, args: Bundle?): Loader { - if(!activity.isFinishing) { + if (!activity.isFinishing) { dialog = AlertDialog.Builder(activity, com.instructure.pandautils.R.style.CustomViewAlertDialog) - .setView(com.instructure.pandautils.R.layout.dialog_loading_view) - .create() + .setView(com.instructure.pandautils.R.layout.dialog_loading_view) + .create() dialog!!.show() } return OpenMediaAsyncTaskLoader(activity, args) @@ -371,14 +591,23 @@ object RouteMatcher : BaseRouteMatcher() { try { if (loadedMedia.isError) { if (loadedMedia.errorType == OpenMediaAsyncTaskLoader.ErrorType.NO_APPS) { - val args = ViewUnsupportedFileFragment.newInstance(loadedMedia.intent!!.data!!, (loader as OpenMediaAsyncTaskLoader).filename!!, loadedMedia.intent!!.type!!, null, R.drawable.ic_attachment).nonNullArgs - RouteMatcher.route(activity, Route(ViewUnsupportedFileFragment::class.java, null, args)) + val args = ViewUnsupportedFileFragment.newInstance( + loadedMedia.intent!!.data!!, + (loader as OpenMediaAsyncTaskLoader).filename!!, + loadedMedia.intent!!.type!!, + null, + R.drawable.ic_attachment + ).nonNullArgs + route(activity, Route(ViewUnsupportedFileFragment::class.java, null, args)) } else { Toast.makeText(activity, activity.resources.getString(loadedMedia.errorMessage), Toast.LENGTH_LONG).show() } } else if (loadedMedia.isHtmlFile) { - val args = ViewHtmlFragment.newInstance(loadedMedia.bundle!!.getString(Const.INTERNAL_URL)!!, loadedMedia.bundle!!.getString(Const.ACTION_BAR_TITLE)!!).nonNullArgs - RouteMatcher.route(activity, Route(ViewHtmlFragment::class.java, null, args)) + val args = ViewHtmlFragment.newInstance( + loadedMedia.bundle!!.getString(Const.INTERNAL_URL)!!, + loadedMedia.bundle!!.getString(Const.ACTION_BAR_TITLE)!! + ).nonNullArgs + route(activity, Route(ViewHtmlFragment::class.java, null, args)) } else if (loadedMedia.intent != null) { if (loadedMedia.intent!!.type!!.contains("pdf") && !loadedMedia.isUseOutsideApps) { // Show pdf with PSPDFkit @@ -386,12 +615,24 @@ object RouteMatcher : BaseRouteMatcher() { val submissionTarget = loadedMedia.bundle?.getParcelable(Const.SUBMISSION_TARGET) FileUtils.showPdfDocument(uri!!, loadedMedia, activity, submissionTarget) } else if (loadedMedia.intent?.type == "video/mp4") { - val bundle = BaseViewMediaActivity.makeBundle(loadedMedia.intent!!.data!!.toString(), null, "video/mp4", loadedMedia.intent!!.dataString, true) - RouteMatcher.route(activity, Route(bundle, RouteContext.MEDIA)) + val bundle = BaseViewMediaActivity.makeBundle( + loadedMedia.intent!!.data!!.toString(), + null, + "video/mp4", + loadedMedia.intent!!.dataString, + true + ) + route(activity, Route(bundle, RouteContext.MEDIA)) } else if (loadedMedia.intent?.type?.startsWith("image/") == true) { - val args = ViewImageFragment.newInstance(loadedMedia.intent!!.dataString!!, loadedMedia.intent!!.data!!, "image/*", true, 0).nonNullArgs - RouteMatcher.route(activity, Route(ViewImageFragment::class.java, null, args)) + val args = ViewImageFragment.newInstance( + loadedMedia.intent!!.dataString!!, + loadedMedia.intent!!.data!!, + "image/*", + true, + 0 + ).nonNullArgs + route(activity, Route(ViewImageFragment::class.java, null, args)) } else { activity.startActivity(loadedMedia.intent) } @@ -415,10 +656,55 @@ object RouteMatcher : BaseRouteMatcher() { if (activity != null) { openMediaCallbacks = null openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(url) - LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID) + LoaderUtils.restartLoaderWithBundle( + LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID + ) + } + } + + private fun handleSpecificFile(activity: FragmentActivity, fileID: String?, route: Route, isGroupFile: Boolean) { + if (fileID == null || offlineDb == null) { + Toast.makeText(activity, R.string.fileNotFound, Toast.LENGTH_SHORT).show() + return + } + + activity.lifecycleScope.tryLaunch { + val fileFolder = if (networkStateProvider?.isOnline() == true) { + FileFolderManager.getFileFolderFromUrlAsync("files/$fileID", true).await().dataOrNull + } else { + getFileFolderFromURL(fileID.toLong(), offlineDb!!) + } + + if (fileFolder == null) { + Toast.makeText(activity, R.string.fileNotFound, Toast.LENGTH_SHORT).show() + return@tryLaunch + } + + if (!isGroupFile && (fileFolder.isLocked || fileFolder.isLockedForUser)) { + val fileName = if (fileFolder.displayName == null) activity.getString(R.string.file) else fileFolder.displayName + Toast.makeText(activity, String.format(activity.getString(R.string.fileLocked), fileName), Toast.LENGTH_LONG).show() + } else { + // This is either a group file (which have no permissions), or a file that is accessible by the user + if (networkStateProvider?.isOnline() == true) { + openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!, route, fileID) + } else { + openLocalMedia(activity, fileFolder.contentType, fileFolder.url, fileFolder.displayName, route.canvasContext!!) + } + } + } catch { + Toast.makeText(activity, R.string.fileNotFound, Toast.LENGTH_SHORT).show() } } + suspend fun getFileFolderFromURL(fileId: Long, offlineDatabase: OfflineDatabase): FileFolder? { + val fileFolderDao = offlineDatabase.fileFolderDao() + val localFileFolderDao = offlineDatabase.localFileDao() + + val file = fileFolderDao.findById(fileId) ?: return null + val localFile = localFileFolderDao.findById(fileId) ?: return null + return file.copy(url = localFile.path).toApiModel() + } + private fun openMedia(activity: FragmentActivity?, mime: String, url: String, filename: String, route: Route, fileId: String?) { if (activity == null) { return @@ -434,32 +720,26 @@ object RouteMatcher : BaseRouteMatcher() { } else { openMediaCallbacks = null openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(mime, url, filename, route.arguments) - LoaderUtils.restartLoaderWithBundle>(LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID) + LoaderUtils.restartLoaderWithBundle>( + LoaderManager.getInstance( + activity + ), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID + ) } } - private fun handleSpecificFile(activity: FragmentActivity, fileID: String?, route: Route, isGroupFile: Boolean) { - - val fileFolderStatusCallback = object : StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - super.onResponse(response, linkHeaders, type) - response.body()?.let { fileFolder -> - if (!isGroupFile && (fileFolder.isLocked || fileFolder.isLockedForUser)) { - Toast.makeText(activity, String.format(activity.getString(R.string.fileLocked), if (fileFolder.displayName == null) activity.getString(R.string.file) else fileFolder.displayName), Toast.LENGTH_LONG).show() - } else { - // This is either a group file (which have no permissions), or a file that is accessible by the user - openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!, route, fileID) - } - } ?: Toast.makeText(activity, activity.getString(R.string.errorOccurred), Toast.LENGTH_LONG).show() - } - - override fun onFail(call: Call?, error: Throwable, response: Response<*>?) { - super.onFail(call, error, response) - Toast.makeText(activity, R.string.fileNotFound, Toast.LENGTH_SHORT).show() - } + private fun openLocalMedia(activity: FragmentActivity?, mime: String?, path: String?, filename: String?, canvasContext: CanvasContext) { + val owner = activity ?: return + onMainThread { + openMediaCallbacks = null + openMediaBundle = OpenMediaAsyncTaskLoader.createLocalBundle(canvasContext, mime, path, filename, false) + LoaderUtils.restartLoaderWithBundle>( + LoaderManager.getInstance(owner), + openMediaBundle, + getLoaderCallbacks(owner), + R.id.openMediaLoaderID + ) } - - FileFolderManager.getFileFolderFromURL("files/$fileID", true, fileFolderStatusCallback) } /** @@ -474,7 +754,7 @@ object RouteMatcher : BaseRouteMatcher() { @JvmOverloads fun canRouteInternally( - context: Context, + activity: FragmentActivity?, url: String, domain: String, routeIfPossible: Boolean, @@ -482,30 +762,33 @@ object RouteMatcher : BaseRouteMatcher() { ): Boolean { val route = getInternalRoute(url, domain) val canRoute = route != null && (allowUnsupported || route.primaryClass != UnsupportedFeatureFragment::class.java) - if (canRoute && routeIfPossible) routeUrl(context, url) + if (canRoute && routeIfPossible) activity?.let { routeUrl(activity, url) } return canRoute } - fun generateUrl(url: String?, queryParams: HashMap): String? { - if(url == null) return null + if (url == null) return null return createQueryParamString(url, queryParams) } - fun generateUrl(type: CanvasContext.Type, masterCls: Class?, replacementParams: HashMap): String? { return generateUrl(type, masterCls, null, replacementParams, null) } - - fun generateUrl(type: CanvasContext.Type, masterCls: Class?, detailCls: Class?, replacementParams: HashMap?, queryParams: HashMap?): String? { + fun generateUrl( + type: CanvasContext.Type, + masterCls: Class?, + detailCls: Class?, + replacementParams: HashMap?, + queryParams: HashMap? + ): String? { val domain = ApiPrefs.fullDomain // Workaround for the discussion details because we bookmark a different class that we use for routing val detailsClass = if (detailCls == DiscussionDetailsFragment::class.java) DiscussionRouterFragment::class.java else detailCls val urlRoute = getInternalRoute(masterCls, detailsClass) - if(urlRoute != null) { + if (urlRoute != null) { var path = urlRoute.createUrl(replacementParams) if (path.contains(COURSE_OR_GROUP_REGEX)) { val pattern = Pattern.compile(COURSE_OR_GROUP_REGEX, Pattern.LITERAL) @@ -530,4 +813,3 @@ object RouteMatcher : BaseRouteMatcher() { initRoutes() } } - diff --git a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt index 12f77b7d11..9b32a52a24 100644 --- a/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt +++ b/apps/student/src/main/java/com/instructure/student/router/RouteResolver.kt @@ -9,12 +9,29 @@ import com.instructure.pandautils.features.discussion.router.DiscussionRouterFra import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.pandautils.features.notification.preferences.EmailNotificationPreferencesFragment import com.instructure.pandautils.features.notification.preferences.PushNotificationPreferencesFragment +import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentFragment +import com.instructure.pandautils.features.offline.sync.progress.SyncProgressFragment import com.instructure.pandautils.utils.Const import com.instructure.student.AnnotationComments.AnnotationCommentListFragment import com.instructure.student.activity.NothingToSeeHereFragment -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.assignments.list.AssignmentListFragment +import com.instructure.student.features.coursebrowser.CourseBrowserFragment +import com.instructure.student.features.discussion.details.DiscussionDetailsFragment +import com.instructure.student.features.discussion.list.DiscussionListFragment import com.instructure.student.features.elementary.course.ElementaryCourseFragment +import com.instructure.student.features.files.details.FileDetailsFragment +import com.instructure.student.features.files.list.FileListFragment import com.instructure.student.features.files.search.FileSearchFragment +import com.instructure.student.features.grades.GradesListFragment +import com.instructure.student.features.modules.list.ModuleListFragment +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment +import com.instructure.student.features.modules.progression.ModuleQuizDecider +import com.instructure.student.features.pages.details.PageDetailsFragment +import com.instructure.student.features.pages.list.PageListFragment +import com.instructure.student.features.people.details.PeopleDetailsFragment +import com.instructure.student.features.people.list.PeopleListFragment +import com.instructure.student.features.quiz.list.QuizListFragment import com.instructure.student.fragment.* import com.instructure.student.mobius.assignmentDetails.submission.annnotation.AnnotationSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.file.ui.UploadStatusSubmissionFragment @@ -22,11 +39,11 @@ import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.Pic import com.instructure.student.mobius.assignmentDetails.submission.text.ui.TextSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submission.url.ui.UrlSubmissionUploadFragment import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.rubric.ui.SubmissionRubricDescriptionFragment -import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment -import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsFragment -import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment +import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsRepositoryFragment +import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsRepositoryFragment +import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment import com.instructure.student.mobius.elementary.ElementaryDashboardFragment -import com.instructure.student.mobius.syllabus.ui.SyllabusFragment +import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment object RouteResolver { @@ -84,13 +101,13 @@ object RouteResolver { cls.isA() -> PageDetailsFragment.newInstance(route) cls.isA() -> LtiLaunchFragment.newInstance(route) cls.isA() -> CreateAnnouncementFragment.newInstance(route) - cls.isA() -> SyllabusFragment.newInstance(route) + cls.isA() -> SyllabusRepositoryFragment.newInstance(route) cls.isA() -> GradesListFragment.newInstance(route) cls.isA() -> ModuleListFragment.newInstance(route) cls.isA() -> CourseSettingsFragment.newInstance(route) cls.isA() -> AnnouncementListFragment.newInstance(route) - cls.isA() -> ConferenceDetailsFragment.newInstance(route) - cls.isA() -> ConferenceListFragment.newInstance(route) + cls.isA() -> ConferenceDetailsRepositoryFragment.newInstance(route) + cls.isA() -> ConferenceListRepositoryFragment.newInstance(route) cls.isA() -> UnsupportedTabFragment.newInstance(route) cls.isA() -> PageListFragment.newInstance(route) cls.isA() -> UnsupportedFeatureFragment.newInstance(route) @@ -109,7 +126,7 @@ object RouteResolver { cls.isA() -> AccountPreferencesFragment.newInstance() cls.isA() -> CourseModuleProgressionFragment.newInstance(route) cls.isA() -> AssignmentDetailsFragment.newInstance(route) - cls.isA() -> SubmissionDetailsFragment.newInstance(route) + cls.isA() -> SubmissionDetailsRepositoryFragment.newInstance(route) cls.isA() -> SubmissionRubricDescriptionFragment.newInstance(route) cls.isA() -> DiscussionListFragment.newInstance(route) cls.isA() -> DiscussionDetailsFragment.newInstance(route) @@ -128,6 +145,8 @@ object RouteResolver { cls.isA() -> EmailNotificationPreferencesFragment.newInstance() cls.isA() -> DiscussionDetailsWebViewFragment.newInstance(route) cls.isA() -> DiscussionRouterFragment.newInstance(route.canvasContext!!, route) + cls.isA() -> OfflineContentFragment.newInstance(route) + cls.isA() -> SyncProgressFragment.newInstance() cls.isA() -> InternalWebviewFragment.newInstance(route) // Keep this at the end else -> null } diff --git a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt index c24c3d17dc..8446c3f04b 100644 --- a/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt +++ b/apps/student/src/main/java/com/instructure/student/tasks/StudentLogoutTask.kt @@ -21,19 +21,23 @@ import android.content.Intent import android.net.Uri import com.google.firebase.messaging.FirebaseMessaging import com.heapanalytics.android.Heap +import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.canvasapi2.utils.tryOrNull import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.student.activity.LoginActivity import com.instructure.student.flutterChannels.FlutterComm import com.instructure.student.util.StudentPrefs import com.instructure.student.widget.WidgetUpdater +import java.io.File class StudentLogoutTask( type: Type, uri: Uri? = null, canvasForElementaryFeatureFlag: Boolean = false, - typefaceBehavior: TypefaceBehavior? = null + typefaceBehavior: TypefaceBehavior? = null, + private val databaseProvider: DatabaseProvider? = null ) : LogoutTask(type, uri, canvasForElementaryFeatureFlag, typefaceBehavior) { override fun onCleanup() { @@ -47,7 +51,7 @@ class StudentLogoutTask( return LoginActivity.createIntent(context) } - override fun createQRLoginIntent(context: Context, uri: Uri): Intent? { + override fun createQRLoginIntent(context: Context, uri: Uri): Intent { return LoginActivity.createIntent(context, uri) } @@ -60,4 +64,12 @@ class StudentLogoutTask( listener(registrationId) } } + + override fun removeOfflineData(userId: Long?) { + userId?.let { + val dir = File(ContextKeeper.appContext.filesDir, it.toString()) + dir.deleteRecursively() + databaseProvider?.clearDatabase(it) + } + } } diff --git a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt index feea6a0936..20a7c1fa77 100644 --- a/apps/student/src/main/java/com/instructure/student/util/AppManager.kt +++ b/apps/student/src/main/java/com/instructure/student/util/AppManager.kt @@ -18,10 +18,10 @@ package com.instructure.student.util import androidx.hilt.work.HiltWorkerFactory -import androidx.work.Configuration import androidx.work.WorkerFactory import com.instructure.canvasapi2.utils.MasqueradeHelper import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.room.offline.DatabaseProvider import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.student.tasks.StudentLogoutTask import dagger.hilt.android.HiltAndroidApp @@ -36,13 +36,18 @@ class AppManager : BaseAppManager() { @Inject lateinit var workerFactory: HiltWorkerFactory + @Inject + lateinit var databaseProvider: DatabaseProvider + override fun onCreate() { super.onCreate() - MasqueradeHelper.masqueradeLogoutTask = Runnable { StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior).execute() } + MasqueradeHelper.masqueradeLogoutTask = Runnable { + StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider).execute() + } } override fun performLogoutOnAuthError() { - StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior).execute() + StudentLogoutTask(LogoutTask.Type.LOGOUT, typefaceBehavior = typefaceBehavior, databaseProvider = databaseProvider).execute() } override fun getWorkManagerFactory(): WorkerFactory = workerFactory diff --git a/apps/student/src/main/java/com/instructure/student/util/Const.kt b/apps/student/src/main/java/com/instructure/student/util/Const.kt index 811ebb93a5..37fc65f8ae 100644 --- a/apps/student/src/main/java/com/instructure/student/util/Const.kt +++ b/apps/student/src/main/java/com/instructure/student/util/Const.kt @@ -24,6 +24,7 @@ object Const { const val CANVAS_CONTEXT = "canvasContext" const val COURSE = "course" const val FILE_URL = "fileUrl" + const val FILE_ID = "fileId" const val ID = "id" const val INTERNAL_URL = "internalURL" const val MESSAGE = "message" diff --git a/apps/student/src/main/java/com/instructure/student/util/ModuleUtility.kt b/apps/student/src/main/java/com/instructure/student/util/ModuleUtility.kt deleted file mode 100644 index bcb221e07d..0000000000 --- a/apps/student/src/main/java/com/instructure/student/util/ModuleUtility.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2016 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.student.util - -import android.net.Uri -import androidx.fragment.app.Fragment -import com.instructure.canvasapi2.models.Course -import com.instructure.canvasapi2.models.ModuleItem -import com.instructure.canvasapi2.models.ModuleObject -import com.instructure.canvasapi2.utils.APIHelper.expandTildeId -import com.instructure.canvasapi2.utils.findWithPrevious -import com.instructure.canvasapi2.utils.isLocked -import com.instructure.interactions.router.Route -import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment.Companion.makeRoute -import com.instructure.student.fragment.* -import com.instructure.student.fragment.DiscussionDetailsFragment.Companion.makeRoute -import com.instructure.student.fragment.InternalWebviewFragment.Companion.makeRoute -import com.instructure.student.fragment.LockedModuleItemFragment.Companion.makeRoute -import com.instructure.student.fragment.MasteryPathSelectionFragment.Companion.makeRoute -import com.instructure.student.fragment.PageDetailsFragment.Companion.makeRoute -import java.util.* - -object ModuleUtility { - fun getFragment(item: ModuleItem, course: Course, moduleObject: ModuleObject?, isDiscussionRedesignEnabled: Boolean, navigatedFromModules: Boolean): Fragment? = when (item.type) { - "Page" -> PageDetailsFragment.newInstance(makeRoute(course, item.title, item.pageUrl, navigatedFromModules)) - "Assignment" -> AssignmentDetailsFragment.newInstance(makeRoute(course, getAssignmentId(item))) - "Discussion" -> { - if (isDiscussionRedesignEnabled) { - DiscussionDetailsWebViewFragment.newInstance(getDiscussionRedesignRoute(item, course)) - } else { - DiscussionDetailsFragment.newInstance(getDiscussionRoute(item, course)) - } - } - "Locked" -> LockedModuleItemFragment.newInstance(makeRoute(course, item.title!!, item.moduleDetails?.lockExplanation ?: "")) - "SubHeader" -> null // Don't do anything with headers, they're just dividers so we don't show them here. - "Quiz" -> { - val apiURL = removeDomain(item.url) - ModuleQuizDecider.newInstance(ModuleQuizDecider.makeRoute(course, item.htmlUrl!!, apiURL!!)) - } - "ChooseAssignmentGroup" -> { - val route = makeRoute(course, item.masteryPaths!!, moduleObject!!.id, item.masteryPathsItemId) - MasteryPathSelectionFragment.newInstance(route) - } - "ExternalUrl", "ExternalTool" -> { - if (item.isLocked()) { - LockedModuleItemFragment.newInstance(makeRoute(course, item.title!!, item.moduleDetails?.lockExplanation ?: "")) - } else { - val uri = Uri.parse(item.htmlUrl).buildUpon().appendQueryParameter("display", "borderless").build() - val route = makeRoute(course, uri.toString(), item.title!!, true, true, true) - InternalWebviewFragment.newInstance(route) - } - } - "File" -> { - val url = removeDomain(item.url) - if (moduleObject == null) { - FileDetailsFragment.newInstance(FileDetailsFragment.makeRoute(course, url!!)) - } else { - FileDetailsFragment.newInstance(FileDetailsFragment.makeRoute(course, moduleObject, item.id, url!!)) - } - } - else -> null - } - - fun isGroupLocked(module: ModuleObject?): Boolean { - // NOTE: The state of the group is is "Locked" until the user visits the modules online - // Check if the unlock date has passed - if (module?.unlockDate?.after(Date()) == true) return true - - // Check if the state is Locked AND there are prerequisites - return module?.prerequisiteIds != null && module.state == ModuleObject.State.Locked.apiString - } - - private fun getAssignmentId(moduleItem: ModuleItem): Long { - // Get the assignment id from the url - return Uri.parse(moduleItem.url).pathSegments - .findWithPrevious { previous, _ -> previous == "assignments" } - ?.let { expandTildeId(it) } - ?.toLongOrNull() ?: 0 - } - - private fun getDiscussionRoute(moduleItem: ModuleItem, course: Course): Route { - // Get the topic id from the url - val topicId = Uri.parse(moduleItem.url).pathSegments - .findWithPrevious { previous, _ -> previous == "discussion_topics" } - ?.let { expandTildeId(it) } - ?.toLongOrNull() ?: 0 - return makeRoute(course, topicId, null) - } - - private fun getDiscussionRedesignRoute(moduleItem: ModuleItem, course: Course): Route { - // Get the topic id from the url - val topicId = Uri.parse(moduleItem.url).pathSegments - .findWithPrevious { previous, _ -> previous == "discussion_topics" } - ?.let { expandTildeId(it) } - ?.toLongOrNull() ?: 0 - return DiscussionDetailsWebViewFragment.makeRoute(course, topicId) - } - - /** Strips off the domain and protocol */ - private fun removeDomain(url: String?): String? = url?.substringAfter("/api/v1/") -} diff --git a/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt b/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt index 4140fb63ad..4b77b5f92d 100644 --- a/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt +++ b/apps/student/src/main/java/com/instructure/student/util/TabHelper.kt @@ -27,10 +27,19 @@ import com.instructure.canvasapi2.utils.validOrNull import com.instructure.interactions.router.Route import com.instructure.student.R import com.instructure.student.activity.NothingToSeeHereFragment +import com.instructure.student.features.assignments.list.AssignmentListFragment +import com.instructure.student.features.discussion.list.DiscussionListFragment +import com.instructure.student.features.files.list.FileListFragment +import com.instructure.student.features.grades.GradesListFragment +import com.instructure.student.features.modules.list.ModuleListFragment +import com.instructure.student.features.pages.details.PageDetailsFragment +import com.instructure.student.features.pages.list.PageListFragment +import com.instructure.student.features.people.list.PeopleListFragment +import com.instructure.student.features.quiz.list.QuizListFragment import com.instructure.student.fragment.* -import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment -import com.instructure.student.mobius.syllabus.ui.SyllabusFragment -import java.util.Locale +import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment +import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment +import java.util.* object TabHelper { @@ -92,10 +101,10 @@ object TabHelper { Tab.DISCUSSIONS_ID -> DiscussionListFragment.makeRoute(canvasContext) Tab.PEOPLE_ID -> PeopleListFragment.makeRoute(canvasContext) Tab.FILES_ID -> FileListFragment.makeRoute(canvasContext) - Tab.SYLLABUS_ID -> SyllabusFragment.makeRoute(canvasContext as Course) + Tab.SYLLABUS_ID -> SyllabusRepositoryFragment.makeRoute(canvasContext as Course) Tab.QUIZZES_ID -> QuizListFragment.makeRoute(canvasContext) Tab.OUTCOMES_ID -> UnsupportedTabFragment.makeRoute(canvasContext, tab.tabId) - Tab.CONFERENCES_ID -> ConferenceListFragment.makeRoute(canvasContext) + Tab.CONFERENCES_ID -> ConferenceListRepositoryFragment.makeRoute(canvasContext) Tab.COLLABORATIONS_ID -> UnsupportedTabFragment.makeRoute(canvasContext, tab.tabId) Tab.ANNOUNCEMENTS_ID -> AnnouncementListFragment.makeRoute(canvasContext) Tab.GRADES_ID -> GradesListFragment.makeRoute(canvasContext) diff --git a/apps/student/src/main/res/layout/activity_navigation.xml b/apps/student/src/main/res/layout/activity_navigation.xml index 40ac2489bc..dde7608647 100644 --- a/apps/student/src/main/res/layout/activity_navigation.xml +++ b/apps/student/src/main/res/layout/activity_navigation.xml @@ -31,7 +31,8 @@ android:clipToPadding="false" android:orientation="vertical"> - @@ -43,13 +44,26 @@ - + + + + + diff --git a/apps/student/src/main/res/layout/activity_settings.xml b/apps/student/src/main/res/layout/activity_settings.xml index 0a10a2ed51..7a115143d5 100644 --- a/apps/student/src/main/res/layout/activity_settings.xml +++ b/apps/student/src/main/res/layout/activity_settings.xml @@ -15,16 +15,34 @@ ~ along with this program. If not, see . ~ --> - - + android:layout_height="wrap_content" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent"/> - + + + + + + + diff --git a/apps/student/src/main/res/layout/fragment_application_settings.xml b/apps/student/src/main/res/layout/fragment_application_settings.xml index 23a0f25ae9..cfc219aaae 100644 --- a/apps/student/src/main/res/layout/fragment_application_settings.xml +++ b/apps/student/src/main/res/layout/fragment_application_settings.xml @@ -1,5 +1,4 @@ - - + tools:text="Subtitle" /> @@ -116,7 +116,7 @@ android:id="@+id/courseBrowserHeader" layout="@layout/view_course_browser_header" android:layout_width="match_parent" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" /> @@ -141,7 +141,7 @@ android:layout_height="match_parent" android:cacheColorHint="@android:color/transparent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_behavior="@string/appbar_scrolling_view_behavior"/> + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> @@ -149,7 +149,7 @@ android:id="@+id/emptyView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_centerInParent="true"/> + android:layout_centerInParent="true" /> diff --git a/apps/student/src/main/res/layout/fragment_not_available_offline.xml b/apps/student/src/main/res/layout/fragment_not_available_offline.xml new file mode 100644 index 0000000000..1e71fc297a --- /dev/null +++ b/apps/student/src/main/res/layout/fragment_not_available_offline.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + diff --git a/apps/student/src/main/res/layout/navigation_drawer.xml b/apps/student/src/main/res/layout/navigation_drawer.xml index bfe6e6002f..d4fc78d715 100644 --- a/apps/student/src/main/res/layout/navigation_drawer.xml +++ b/apps/student/src/main/res/layout/navigation_drawer.xml @@ -15,6 +15,7 @@ ~ --> + + + + + + + + diff --git a/apps/student/src/main/res/layout/view_offline_indicator.xml b/apps/student/src/main/res/layout/view_offline_indicator.xml new file mode 100644 index 0000000000..746cc0dcd1 --- /dev/null +++ b/apps/student/src/main/res/layout/view_offline_indicator.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml b/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml index 15cf56ca1a..fb89c22521 100644 --- a/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml +++ b/apps/student/src/main/res/layout/view_student_enhanced_grade_cell.xml @@ -21,7 +21,7 @@ + type="com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData" /> diff --git a/apps/student/src/main/res/layout/viewholder_course_card.xml b/apps/student/src/main/res/layout/viewholder_course_card.xml index 2ff4a378f0..2f0f0aa263 100644 --- a/apps/student/src/main/res/layout/viewholder_course_card.xml +++ b/apps/student/src/main/res/layout/viewholder_course_card.xml @@ -77,7 +77,7 @@ - + + @@ -110,10 +122,12 @@ android:maxLines="2" android:textColor="@color/textDark" android:textSize="12sp" + app:layout_constraintTop_toBottomOf="@id/titleTextView" + app:layout_constraintStart_toStartOf="parent" tools:ignore="Deprecated" tools:text="LIT 401" /> - + + android:text="@string/dashboardAllCoursesButton"/> diff --git a/apps/student/src/main/res/menu/menu_dashboard.xml b/apps/student/src/main/res/menu/menu_dashboard.xml index c2ae207142..e6b864ed0c 100644 --- a/apps/student/src/main/res/menu/menu_dashboard.xml +++ b/apps/student/src/main/res/menu/menu_dashboard.xml @@ -21,6 +21,11 @@ android:id="@+id/menu_dashboard_cards" android:icon="@drawable/ic_list_dashboard" android:title="@string/dashboardSwitchToListView" - app:showAsAction="always" /> + app:showAsAction="never" /> + + \ No newline at end of file diff --git a/apps/student/src/qa/java/com/instructure/student/SingleFragmentTestActivity.kt b/apps/student/src/qa/java/com/instructure/student/SingleFragmentTestActivity.kt index 9178d65b43..da4dbeb494 100644 --- a/apps/student/src/qa/java/com/instructure/student/SingleFragmentTestActivity.kt +++ b/apps/student/src/qa/java/com/instructure/student/SingleFragmentTestActivity.kt @@ -25,7 +25,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.instructure.pandautils.binding.viewBinding import com.instructure.student.databinding.ActivitySingleFragmentTestBinding +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class SingleFragmentTestActivity : AppCompatActivity() { private val binding by viewBinding(ActivitySingleFragmentTestBinding::inflate) diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentdetail/AssignmentDetailsViewModelTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt similarity index 50% rename from apps/student/src/test/java/com/instructure/student/features/assignmentdetail/AssignmentDetailsViewModelTest.kt rename to apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt index fb6ca1074a..65c3352f1a 100644 --- a/apps/student/src/test/java/com/instructure/student/features/assignmentdetail/AssignmentDetailsViewModelTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.features.assignmentdetail +package com.instructure.student.features.assignmentdetails import android.app.Application import android.content.res.Resources @@ -25,14 +25,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.SavedStateHandle -import com.instructure.canvasapi2.managers.AssignmentManager -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.QuizManager -import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper -import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.R import com.instructure.pandautils.mvvm.ViewState @@ -40,9 +35,10 @@ import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.HtmlContentFormatter import com.instructure.student.db.StudentDb -import com.instructure.student.features.assignmentdetails.AssignmentDetailAction -import com.instructure.student.features.assignmentdetails.AssignmentDetailsViewModel -import com.instructure.student.features.assignmentdetails.gradecellview.GradeCellViewData +import com.instructure.student.features.assignments.details.AssignmentDetailAction +import com.instructure.student.features.assignments.details.AssignmentDetailsRepository +import com.instructure.student.features.assignments.details.AssignmentDetailsViewModel +import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData import io.mockk.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -66,10 +62,7 @@ class AssignmentDetailsViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) - private val courseManager: CourseManager = mockk(relaxed = true) - private val assignmentManager: AssignmentManager = mockk(relaxed = true) - private val quizManager: QuizManager = mockk(relaxed = true) - private val submissionManager: SubmissionManager = mockk(relaxed = true) + private val assignmentDetailsRepository: AssignmentDetailsRepository = mockk(relaxed = true) private val resources: Resources = mockk(relaxed = true) private val htmlContentFormatter: HtmlContentFormatter = mockk(relaxed = true) private val colorKeeper: ColorKeeper = mockk(relaxed = true) @@ -100,10 +93,7 @@ class AssignmentDetailsViewModelTest { private fun getViewModel() = AssignmentDetailsViewModel( savedStateHandle, - courseManager, - assignmentManager, - quizManager, - submissionManager, + assignmentDetailsRepository, resources, htmlContentFormatter, colorKeeper, @@ -136,9 +126,7 @@ class AssignmentDetailsViewModelTest { every { resources.getString(R.string.errorLoadingAssignment) } returns expected - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } throws IllegalStateException() val viewModel = getViewModel() @@ -146,21 +134,29 @@ class AssignmentDetailsViewModelTest { Assert.assertEquals(expected, (viewModel.state.value as? ViewState.Error)?.errorMessage) } + @Test + fun `Authentication error`() { + val generalError = "There was a problem loading this assignment. Please check your connection and try again." + val authError = "This assignment is no longer available." + + every { resources.getString(R.string.errorLoadingAssignment) } returns generalError + every { resources.getString(R.string.assignmentNoLongerAvailable) } returns authError + + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } throws IllegalAccessException() + + val viewModel = getViewModel() + + Assert.assertEquals(ViewState.Error(authError), viewModel.state.value) + Assert.assertEquals(authError, (viewModel.state.value as? ViewState.Error)?.errorMessage) + } + @Test fun `Load fully locked assignment`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } - - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - lockInfo = LockInfo( - unlockAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString() - ) - ) - ) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val assignment = Assignment(lockInfo = LockInfo(unlockAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.time.toApiString())) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -174,18 +170,14 @@ class AssignmentDetailsViewModelTest { every { resources.getString(R.string.errorLoadingAssignment) } returns lockedExplanation - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - lockExplanation = lockedExplanation, - lockAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.time.toApiString() - ) - ) - } + val assignment = Assignment( + lockExplanation = lockedExplanation, + lockAt = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.time.toApiString() + ) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -198,13 +190,11 @@ class AssignmentDetailsViewModelTest { fun `Load assignment as Student`() { val expected = "Test name" - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(name = expected)) - } + val assignment = Assignment(name = expected) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -216,18 +206,11 @@ class AssignmentDetailsViewModelTest { fun `Load assignment as Parent`() { val expected = "Test name" - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Observer)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Observer))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentIncludeObserveesAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - ObserveeAssignment( - submissionList = listOf(Submission()), - name = expected - ) - ) - } + val assignment = Assignment(name = expected) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -237,13 +220,10 @@ class AssignmentDetailsViewModelTest { @Test fun `Load assignment with draft`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() every { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() @@ -257,23 +237,19 @@ class AssignmentDetailsViewModelTest { @Test fun `Map attempts`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } - - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - submission = Submission( - submissionHistory = listOf( - Submission(submittedAt = Date()), - Submission(submittedAt = Date()), - Submission(submittedAt = Date()) - ) - ) + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val assignment = Assignment( + submission = Submission( + submissionHistory = listOf( + Submission(submittedAt = Date()), + Submission(submittedAt = Date()), + Submission(submittedAt = Date()) ) ) - } + ) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -290,13 +266,11 @@ class AssignmentDetailsViewModelTest { false ) - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submission = Submission())) - } + val assignment = Assignment(submission = Submission()) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -312,18 +286,14 @@ class AssignmentDetailsViewModelTest { every { resources.getString(R.string.missingAssignment) } returns expectedLabelText - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - submission = Submission(missing = true), - submissionTypesRaw = listOf("media_recording") - ) - ) - } + val assignment = Assignment( + submission = Submission(missing = true), + submissionTypesRaw = listOf("media_recording") + ) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -341,13 +311,11 @@ class AssignmentDetailsViewModelTest { every { resources.getString(R.string.notSubmitted) } returns expectedLabelText - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("media_recording"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("media_recording")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -365,21 +333,17 @@ class AssignmentDetailsViewModelTest { every { resources.getString(R.string.gradedSubmissionLabel) } returns expectedLabelText - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } - - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - submission = Submission( - submittedAt = Date(), - grade = "A", - postedAt = Date() - ) - ) + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val assignment = Assignment( + submission = Submission( + submittedAt = Date(), + grade = "A", + postedAt = Date() ) - } + ) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -397,18 +361,14 @@ class AssignmentDetailsViewModelTest { every { resources.getString(R.string.submitted) } returns expectedLabelText - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - submissionTypesRaw = listOf("media_recording"), - submission = Submission(submittedAt = Date()) - ) - ) - } + val assignment = Assignment( + submissionTypesRaw = listOf("media_recording"), + submission = Submission(submittedAt = Date()) + ) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() @@ -440,13 +400,10 @@ class AssignmentDetailsViewModelTest { false ) - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignment) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onAttemptSelected(2) @@ -459,13 +416,10 @@ class AssignmentDetailsViewModelTest { fun `LTI button click`() { val expected = "test" - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() val viewModel = getViewModel() viewModel.onLtiButtonPressed(expected) @@ -475,13 +429,10 @@ class AssignmentDetailsViewModelTest { @Test fun `Grade cell click`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() val viewModel = getViewModel() viewModel.onGradeCellClicked() @@ -491,13 +442,10 @@ class AssignmentDetailsViewModelTest { @Test fun `Grade cell click while uploading text`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() every { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() @@ -512,13 +460,10 @@ class AssignmentDetailsViewModelTest { @Test fun `Grade cell click while uploading file`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() every { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() @@ -533,13 +478,10 @@ class AssignmentDetailsViewModelTest { @Test fun `Grade cell click while uploading url`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() every { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() @@ -554,17 +496,13 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - quiz`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("online_quiz"), quizId = 1)) - } + val assignment = Assignment(submissionTypesRaw = listOf("online_quiz"), quizId = 1) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment - every { quizManager.getQuizAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Quiz()) - } + coEvery { assignmentDetailsRepository.getQuiz(any(), any(), any()) } returns Quiz() val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -574,13 +512,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - discussion`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("discussion_topic"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("discussion_topic")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -590,21 +526,17 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - multiple submission types`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } - - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success( - Assignment( - submissionTypesRaw = listOf( - "online_text_entry", - "online_url", - "media_recording" - ) - ) + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + val assignment = Assignment( + submissionTypesRaw = listOf( + "online_text_entry", + "online_url", + "media_recording" ) - } + ) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -614,13 +546,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - text`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("online_text_entry"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("online_text_entry")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -630,13 +560,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - url`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("online_url"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("online_url")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -646,13 +574,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - annotation`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("student_annotation"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("student_annotation")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -662,13 +588,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - media recoding`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("media_recording"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("media_recording")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -678,13 +602,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Submit button click - external tool`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submissionTypesRaw = listOf("external_tool"))) - } + val assignment = Assignment(submissionTypesRaw = listOf("external_tool")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment val viewModel = getViewModel() viewModel.onSubmitButtonClicked() @@ -694,13 +616,10 @@ class AssignmentDetailsViewModelTest { @Test fun `Upload fail`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() every { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() @@ -724,13 +643,10 @@ class AssignmentDetailsViewModelTest { fun `Upload success`() { val expected = Submission(submittedAt = Date()) - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment()) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment() every { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() @@ -745,9 +661,8 @@ class AssignmentDetailsViewModelTest { database.submissionQueries.getSubmissionsByAssignmentId(any(), any()).executeAsList() } returns listOf() - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submission = Submission(submissionHistory = listOf(expected)))) - } + val assignment = Assignment(submission = Submission(submissionHistory = listOf(expected))) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment viewModel.queryResultsChanged() @@ -756,13 +671,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Create viewData with points when quantitative data is not restricted`() { - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)))) - } + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns Course( + enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)) + ) - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submission = Submission(), pointsPossible = 20.0)) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment(submission = Submission(), pointsPossible = 20.0) every { resources.getQuantityString(any(), any(), any()) } returns "20 pts" @@ -774,13 +687,11 @@ class AssignmentDetailsViewModelTest { @Test fun `Create viewData without points when quantitative data is restricted`() { val courseSettings = CourseSettings(restrictQuantitativeData = true) - every { courseManager.getCourseWithGradeAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)), settings = courseSettings)) - } + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns Course( + enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student)), settings = courseSettings + ) - every { assignmentManager.getAssignmentWithHistoryAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(Assignment(submission = Submission(), pointsPossible = 20.0)) - } + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment(submission = Submission(), pointsPossible = 20.0) every { resources.getQuantityString(any(), any(), any()) } returns "20 pts" @@ -788,4 +699,28 @@ class AssignmentDetailsViewModelTest { Assert.assertEquals("", viewModel.data.value?.points) } + + @Test + fun `Do not show content if assignment is null`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } throws IllegalStateException() + + val viewModel = getViewModel() + + Assert.assertFalse(viewModel.showContent(viewModel.state.value)) + } + + @Test + fun `Show content on assignment success`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns Assignment(submission = Submission(), pointsPossible = 20.0) + + val viewModel = getViewModel() + + Assert.assertTrue(viewModel.showContent(viewModel.state.value)) + } } diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentdetail/gradecellview/GradeCellViewDataTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/gradecellview/GradeCellViewDataTest.kt similarity index 97% rename from apps/student/src/test/java/com/instructure/student/features/assignmentdetail/gradecellview/GradeCellViewDataTest.kt rename to apps/student/src/test/java/com/instructure/student/features/assignmentdetails/gradecellview/GradeCellViewDataTest.kt index ce09c567fa..9c048e9db2 100644 --- a/apps/student/src/test/java/com/instructure/student/features/assignmentdetail/gradecellview/GradeCellViewDataTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/gradecellview/GradeCellViewDataTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.student.features.assignmentdetail.gradecellview +package com.instructure.student.features.assignmentdetails.gradecellview import android.content.res.Resources import com.instructure.canvasapi2.models.Assignment @@ -24,7 +24,7 @@ import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.models.Submission import com.instructure.pandautils.R import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.student.features.assignmentdetails.gradecellview.GradeCellViewData +import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData import io.mockk.every import io.mockk.mockk import org.junit.Assert diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt new file mode 100644 index 0000000000..d328763653 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/AssignmentListRepositoryTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignmentlist + +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.assignments.list.AssignmentListRepository +import com.instructure.student.features.assignments.list.datasource.AssignmentListLocalDataSource +import com.instructure.student.features.assignments.list.datasource.AssignmentListNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class AssignmentListRepositoryTest { + + private val networkDataSource: AssignmentListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: AssignmentListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = AssignmentListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get assignment groups with assignments for grading period if device is online`() = runTest { + val expected = listOf(AssignmentGroup(id = 1), AssignmentGroup(id = 2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns expected + + val result = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + coVerify { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) } + assertEquals(expected, result) + } + + @Test + fun `Get assignment groups with assignments for grading period if device is offline`() = runTest { + val expected = listOf(AssignmentGroup(id = 1), AssignmentGroup(id = 2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns expected + + val result = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + coVerify { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) } + assertEquals(expected, result) + } + + @Test + fun `Get assignment groups with assignments if device is online`() = runTest { + val expected = listOf(AssignmentGroup(id = 1), AssignmentGroup(id = 2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns expected + + val result = repository.getAssignmentGroupsWithAssignments(1, true) + + coVerify { networkDataSource.getAssignmentGroupsWithAssignments(1, true) } + assertEquals(expected, result) + } + + @Test + fun `Get assignment groups with assignments if device is offline`() = runTest { + val expected = listOf(AssignmentGroup(id = 1), AssignmentGroup(id = 2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns expected + + val result = repository.getAssignmentGroupsWithAssignments(1, true) + + coVerify { localDataSource.getAssignmentGroupsWithAssignments(1, true) } + assertEquals(expected, result) + } + + @Test + fun `Get grading periods if device is online`() = runTest { + val expected = listOf(GradingPeriod(id = 1L), GradingPeriod(id = 2L)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getGradingPeriodsForCourse(any(), any()) } returns expected + + val result = repository.getGradingPeriodsForCourse(1, true) + + coVerify { networkDataSource.getGradingPeriodsForCourse(1, true) } + assertEquals(expected, result) + } + + @Test + fun `Get grading periods if device is offline`() = runTest { + val expected = listOf(GradingPeriod(id = 1L), GradingPeriod(id = 2L)) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getGradingPeriodsForCourse(any(), any()) } returns expected + + val result = repository.getGradingPeriodsForCourse(1, true) + + coVerify { localDataSource.getGradingPeriodsForCourse(1, true) } + assertEquals(expected, result) + } + + @Test + fun `Get course from local storage when device is offline`() = runTest { + coEvery { networkDataSource.getCourseWithGrade(any(), any()) } returns Course(id = 1L, name = "Course 1") + coEvery { localDataSource.getCourseWithGrade(any(), any()) } returns Course(id = 2L, name = "Course 2") + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getCourseWithGrade(1, true) + + Assert.assertEquals(2L, result!!.id) + } + + @Test + fun `Get course from network when device is online`() = runTest { + coEvery { networkDataSource.getCourseWithGrade(any(), any()) } returns Course(id = 1L, name = "Course 1") + coEvery { localDataSource.getCourseWithGrade(any(), any()) } returns Course(id = 2L, name = "Course 2") + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getCourseWithGrade(1, true) + + Assert.assertEquals(1L, result!!.id) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt new file mode 100644 index 0000000000..8baf7cfae7 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListLocalDataSourceTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignmentlist.datasource + +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.student.features.assignments.list.datasource.AssignmentListLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class AssignmentListLocalDataSourceTest { + + private val assignmentFacade: AssignmentFacade = mockk(relaxed = true) + private val courseFacade: CourseFacade = mockk(relaxed = true) + private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + + private val dataSource = AssignmentListLocalDataSource(assignmentFacade, courseFacade, courseSettingsDao) + + @Test + fun `Get assignment groups with assignments for grading period successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1L), AssignmentGroup(2L)) + + coEvery { assignmentFacade.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any()) } returns expected + + val result = dataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + assertEquals(expected, result) + } + + @Test + fun `Get assignment groups with assignments successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1L), AssignmentGroup(2L)) + + coEvery { assignmentFacade.getAssignmentGroupsWithAssignments(any()) } returns expected + + val result = dataSource.getAssignmentGroupsWithAssignments(1, true) + + assertEquals(expected, result) + } + + @Test + fun `Get grading periods successfully returns api model`() = runTest { + val expected = listOf(GradingPeriod(1L), GradingPeriod(2L)) + + coEvery { courseFacade.getGradingPeriodsByCourseId(any()) } returns expected + + val result = dataSource.getGradingPeriodsForCourse(1, true) + + assertEquals(expected, result) + } + + @Test + fun `Get course with grade successfully returns api model`() = runTest { + val expected = Course(1L) + + coEvery { courseFacade.getCourseById(any()) } returns expected + + val result = dataSource.getCourseWithGrade(1, true) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..3c132a2709 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/assignmentlist/datasource/AssignmentListNetworkDataSourceTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.assignmentlist.datasource + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.canvasapi2.models.GradingPeriodResponse +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.assignments.list.datasource.AssignmentListNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class AssignmentListNetworkDataSourceTest { + + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val coursesApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + + private val dataSource = AssignmentListNetworkDataSource(assignmentApi, coursesApi) + + @Test + fun `Get assignment groups with assignments for grading period successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1L), AssignmentGroup(2L)) + + coEvery { + assignmentApi.getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = dataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + Assert.assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get assignment groups with assignments for grading period failure throws exception`() = runTest { + coEvery { + assignmentApi.getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + dataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + } + + @Test + fun `Get assignment groups with assignments successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1L), AssignmentGroup(2L)) + + coEvery { assignmentApi.getFirstPageAssignmentGroupListWithAssignments(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getAssignmentGroupsWithAssignments(1, true) + + Assert.assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get assignment groups with assignments failure throws exception`() = runTest { + coEvery { assignmentApi.getFirstPageAssignmentGroupListWithAssignments(any(), any()) } returns DataResult.Fail() + + dataSource.getAssignmentGroupsWithAssignments(1, true) + } + + @Test + fun `Get grading periods successfully returns api model`() = runTest { + val expected = GradingPeriodResponse(listOf(GradingPeriod(1L), GradingPeriod(2L))) + + coEvery { coursesApi.getGradingPeriodsForCourse(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getGradingPeriodsForCourse(1, true) + + Assert.assertEquals(expected.gradingPeriodList, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get grading periods failure throws exception`() = runTest { + coEvery { coursesApi.getGradingPeriodsForCourse(any(), any()) } returns DataResult.Fail() + + dataSource.getGradingPeriodsForCourse(1, true) + } + + @Test + fun `Get course returns succesful api model`() = runTest { + val expected = Course(id = 1L, name = "Course 1") + + coEvery { coursesApi.getCourseWithGrade(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getCourseWithGrade(1, true) + + Assert.assertEquals(expected, result) + } + + @Test + fun `Get course failure returns null`() = runTest { + coEvery { coursesApi.getCourseWithGrade(any(), any()) } returns DataResult.Fail() + + val result = dataSource.getCourseWithGrade(1, true) + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt new file mode 100644 index 0000000000..a3e79dc0c8 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardLocalDataSourceTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.DashboardCardDao +import com.instructure.pandautils.room.offline.entities.DashboardCardEntity +import com.instructure.pandautils.room.offline.facade.CourseFacade +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class DashboardLocalDataSourceTest { + + private val courseFacade: CourseFacade = mockk(relaxed = true) + private val dashboardCardDao: DashboardCardDao = mockk(relaxed = true) + + private val dataSource = DashboardLocalDataSource(courseFacade, dashboardCardDao) + + @Test + fun `GetCourses returns all courses`() = runTest { + val courses = listOf(Course(1), Course(2)) + coEvery { courseFacade.getAllCourses() } returns courses + + val result = dataSource.getCourses(false) + + Assert.assertEquals(courses, result) + } + + @Test + fun `GetGroups returns empty list`() = runTest { + val result = dataSource.getGroups(false) + + Assert.assertEquals(emptyList(), result) + } + + @Test + fun `getDashboardCourses returns list of Dashboard cards if getDashboardCourses is successful`() = runTest { + val dashboardCards = listOf(DashboardCardEntity(DashboardCard(1)), DashboardCardEntity(DashboardCard(2))) + coEvery { dashboardCardDao.findAll() } returns dashboardCards + + val result = dataSource.getDashboardCards(true) + + Assert.assertEquals(listOf(DashboardCard(1), DashboardCard(2)), result) + } + + @Test + fun `saveDashboardCards saves entities to the dao`() = runTest { + val dashboardCards = listOf(DashboardCard(1), DashboardCard(2)) + + dataSource.saveDashboardCards(dashboardCards) + + coVerify { dashboardCardDao.updateEntities(listOf(DashboardCardEntity(DashboardCard(1)), DashboardCardEntity(DashboardCard(2)))) } + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt new file mode 100644 index 0000000000..56eafb9f9b --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardNetworkDataSourceTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class DashboardNetworkDataSourceTest { + + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + + private val dataSource = DashboardNetworkDataSource(courseApi, groupApi, apiPrefs) + + @Test + fun `getCourses returns courses for teacher if we are in the student view`() = runTest { + val teacherCourses = listOf(Course(1), Course(2)) + val studentCourses = listOf(Course(3), Course(4)) + coEvery { courseApi.getFirstPageCoursesTeacher(any()) } returns DataResult.Success(teacherCourses) + coEvery { courseApi.getFirstPageCourses(any()) } returns DataResult.Success(studentCourses) + every { apiPrefs.isStudentView } returns true + + val result = dataSource.getCourses(true) + + Assert.assertEquals(teacherCourses, result) + } + + @Test + fun `getCourses returns courses for student if we are not in the student view`() = runTest { + val teacherCourses = listOf(Course(1), Course(2)) + val studentCourses = listOf(Course(3), Course(4)) + coEvery { courseApi.getFirstPageCoursesTeacher(any()) } returns DataResult.Success(teacherCourses) + coEvery { courseApi.getFirstPageCourses(any()) } returns DataResult.Success(studentCourses) + every { apiPrefs.isStudentView } returns false + + val result = dataSource.getCourses(true) + + Assert.assertEquals(studentCourses, result) + } + + @Test + fun `getCourses returns empty list if it's failed`() = runTest { + coEvery { courseApi.getFirstPageCourses(any()) } returns DataResult.Fail() + every { apiPrefs.isStudentView } returns false + + val result = dataSource.getCourses(true) + + Assert.assertEquals(emptyList(), result) + } + + @Test + fun `getGroups returns empty list if it's failed`() = runTest { + coEvery { groupApi.getFirstPageGroups(any()) } returns DataResult.Fail() + + val result = dataSource.getGroups(true) + + Assert.assertEquals(emptyList(), result) + } + + @Test + fun `getGroups returns correct groups`() = runTest { + val groups = listOf(Group(1), Group(2)) + coEvery { groupApi.getFirstPageGroups(any()) } returns DataResult.Success(groups) + + val result = dataSource.getGroups(true) + + Assert.assertEquals(groups, result) + } + + @Test + fun `Returns list of Dashboard cards if getDashboardCourses is successful`() = runTest { + val expected = listOf(DashboardCard(id = 1), DashboardCard(id = 2)) + coEvery { courseApi.getDashboardCourses(any()) } returns DataResult.Success(expected) + + val result = dataSource.getDashboardCards(true) + + Assert.assertEquals(expected, result) + } + + @Test + fun `Returns empty list if getDashboardCourses is failed`() = runTest { + coEvery { courseApi.getDashboardCourses(any()) } returns DataResult.Fail() + + val result = dataSource.getDashboardCards(true) + + Assert.assertEquals(emptyList(), result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt new file mode 100644 index 0000000000..dd8b0b4f5f --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/DashboardRepositoryTest.kt @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class DashboardRepositoryTest { + + private val networkDataSource: DashboardNetworkDataSource = mockk(relaxed = true) + private val localDataSource: DashboardLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val courseSyncSettingsDao: CourseSyncSettingsDao = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + private val courseDao: CourseDao = mockk(relaxed = true) + + private val repository = DashboardRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, courseSyncSettingsDao, courseDao) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get courses from network if device is online`() = runTest { + val onlineCourses = listOf(Course(1), Course(2)) + val offlineCourses = listOf(Course(3), Course(4)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCourses(any()) } returns onlineCourses + coEvery { localDataSource.getCourses(any()) } returns offlineCourses + + val courses = repository.getCourses(true) + + Assert.assertEquals(onlineCourses, courses) + } + + @Test + fun `Get courses from local database if device is offline`() = runTest { + val onlineCourses = listOf(Course(1), Course(2)) + val offlineCourses = listOf(Course(3), Course(4)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getCourses(any()) } returns onlineCourses + coEvery { localDataSource.getCourses(any()) } returns offlineCourses + + val courses = repository.getCourses(true) + + Assert.assertEquals(offlineCourses, courses) + } + + @Test + fun `Get groups from network if device is online`() = runTest { + val onlineGroups = listOf(Group(1), Group(2)) + val offlineGroups = listOf(Group(3), Group(4)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getGroups(any()) } returns onlineGroups + coEvery { localDataSource.getGroups(any()) } returns offlineGroups + + val groups = repository.getGroups(true) + + Assert.assertEquals(onlineGroups, groups) + } + + @Test + fun `Get groups from local database if device is offline`() = runTest { + val onlineGroups = listOf(Group(1), Group(2)) + val offlineGroups = listOf(Group(3), Group(4)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getGroups(any()) } returns onlineGroups + coEvery { localDataSource.getGroups(any()) } returns offlineGroups + + val groups = repository.getGroups(true) + + Assert.assertEquals(offlineGroups, groups) + } + + @Test + fun `Returns list of Dashboard cards from local database if device is offline`() = runTest { + val onlineCards = listOf(DashboardCard(1), DashboardCard(2)) + val offlineCards = listOf(DashboardCard(3), DashboardCard(4)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getDashboardCards(any()) } returns onlineCards + coEvery { localDataSource.getDashboardCards(any()) } returns offlineCards + + val result = repository.getDashboardCourses(true) + + Assert.assertEquals(offlineCards, result) + } + + @Test + fun `Returns list of Dashboard cards from network if device is online`() = runTest { + val onlineCards = listOf(DashboardCard(1), DashboardCard(2)) + val offlineCards = listOf(DashboardCard(3), DashboardCard(4)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getDashboardCards(any()) } returns onlineCards + coEvery { localDataSource.getDashboardCards(any()) } returns offlineCards + + val result = repository.getDashboardCourses(true) + + Assert.assertEquals(onlineCards, result) + } + + @Test + fun `Returned dashboard courses are saved to the local store`() = runTest { + val onlineCards = listOf(DashboardCard(1), DashboardCard(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getDashboardCards(any()) } returns onlineCards + + repository.getDashboardCourses(true) + + coVerify { localDataSource.saveDashboardCards(onlineCards) } + } + + @Test + fun `Sort dashboard cards by position`() = runTest { + val dashboardCards = listOf( + DashboardCard(id = 1, position = 1), + DashboardCard(id = 2, position = 0), + DashboardCard(id = 3), + DashboardCard(id = 4, position = 2) + ) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getDashboardCards(any()) } returns dashboardCards + + val result = repository.getDashboardCourses(true) + + val expectedResult = listOf( + DashboardCard(id = 2, position = 0), + DashboardCard(id = 1, position = 1), + DashboardCard(id = 4, position = 2), + DashboardCard(id = 3) + ) + + Assert.assertEquals(expectedResult, result) + } + + @Test + fun `Correctly filtered course ids are returned from getSyncedCourseIds`() = runTest { + val entities = listOf( + CourseSyncSettingsEntity(1, "Course 1",true), + CourseSyncSettingsEntity(2, "Course 2",false), + CourseSyncSettingsEntity(3, "Course 3",false, fullFileSync = true), + CourseSyncSettingsEntity(4, "Course 4",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.ANNOUNCEMENTS_ID }), + CourseSyncSettingsEntity(5, "Course 5",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.DISCUSSIONS_ID }), + CourseSyncSettingsEntity(6, "Course 6",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.PAGES_ID }), + ) + coEvery { courseSyncSettingsDao.findAll() } returns entities + coEvery { courseDao.findByIds(any()) } returns entities.map { CourseEntity(Course(it.courseId)) }.filter { it.id != 2L } + + val result = repository.getSyncedCourseIds() + val expectedIds = setOf(1L, 3L, 4L, 5L, 6L) + + Assert.assertEquals(expectedIds, result) + } + + @Test + fun `Do not return course ids that are not synced yet`() = runTest { + val entities = listOf( + CourseSyncSettingsEntity(1, "Course 1",true), + CourseSyncSettingsEntity(2, "Course 2",false), + CourseSyncSettingsEntity(3, "Course 3",false, fullFileSync = true), + CourseSyncSettingsEntity(4, "Course 4",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.ANNOUNCEMENTS_ID }), + CourseSyncSettingsEntity(5, "Course 5",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.DISCUSSIONS_ID }), + CourseSyncSettingsEntity(6, "Course 6",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.PAGES_ID }), + ) + coEvery { courseSyncSettingsDao.findAll() } returns entities + coEvery { courseDao.findByIds(any()) } returns listOf(CourseEntity(Course(1)), CourseEntity(Course(3))) + + val result = repository.getSyncedCourseIds() + val expectedIds = setOf(1L, 3L) + + Assert.assertEquals(expectedIds, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt index 3073d91692..6375440fa3 100644 --- a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/StudentEditDashboardRepositoryTest.kt @@ -18,72 +18,46 @@ package com.instructure.student.features.dashboard.edit import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.managers.CourseManager -import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.Group -import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardLocalDataSource +import com.instructure.student.features.dashboard.edit.datasource.StudentEditDashboardNetworkDataSource import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic -import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Assert.* +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @ExperimentalCoroutinesApi class StudentEditDashboardRepositoryTest { - private val courseManager: CourseManager = mockk(relaxed = true) - private val groupManager: GroupManager = mockk(relaxed = true) - private val repository = StudentEditDashboardRepository(courseManager, groupManager) + private val networkDataSource: StudentEditDashboardNetworkDataSource = mockk(relaxed = true) + private val localDataSource: StudentEditDashboardLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val courseSyncSettingsDao: CourseSyncSettingsDao = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + private val courseDao: CourseDao = mockk(relaxed = true) - @Before - fun setUp() { - mockkStatic("kotlinx.coroutines.AwaitKt") - } - - @Test - fun `Returns courses when fetching courses`() = runBlockingTest { - // Given - val coursesActive = listOf(Course(id = 1L, name = "Course")) - val coursesCompleted = listOf(Course(id = 2L, name = "Course")) - val coursesInvitedOrPending = listOf(Course(id = 3L, name = "Course")) - - val coursesDeferred: Deferred>> = mockk() - every { courseManager.getCoursesByEnrollmentStateAsync(any(), any()) } returns coursesDeferred - coEvery { listOf(coursesDeferred, coursesDeferred, coursesDeferred).awaitAll() } returns listOf( - DataResult.Success(coursesActive), - DataResult.Success(coursesCompleted), - DataResult.Success(coursesInvitedOrPending) - ) - - // When - val result = repository.getCurses() - - // Then - val expected = listOf(coursesActive, coursesCompleted, coursesInvitedOrPending) - assertEquals(expected, result) - } - - @Test - fun `Returns groups when fetching groups`() = runBlockingTest { - // Given - val groups = listOf(Group(id = 1L, name = "Group1")) - every { groupManager.getAllGroupsAsync(any()) } returns mockk { - coEvery { await() } returns DataResult.Success(groups) - } + private val repository = StudentEditDashboardRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, courseSyncSettingsDao, courseDao) - // When - val result = repository.getGroups() - - // Then - assertEquals(groups, result) + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true } @Test @@ -185,4 +159,98 @@ class StudentEditDashboardRepositoryTest { // Then assertFalse(result) } + + @Test + fun `Correctly filtered course ids are returned from getSyncedCourseIds`() = runTest { + val entities = listOf( + CourseSyncSettingsEntity(1, "Course 1",true), + CourseSyncSettingsEntity(2, "Course 2",false), + CourseSyncSettingsEntity(3, "Course 3",false, fullFileSync = true), + CourseSyncSettingsEntity(4, "Course 4",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.ANNOUNCEMENTS_ID }), + CourseSyncSettingsEntity(5, "Course 5",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.DISCUSSIONS_ID }), + CourseSyncSettingsEntity(6, "Course 6",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.PAGES_ID }), + ) + coEvery { courseSyncSettingsDao.findAll() } returns entities + coEvery { courseDao.findByIds(any()) } returns entities.map { CourseEntity(Course(it.courseId)) }.filter { it.id != 2L } + + val result = repository.getSyncedCourseIds() + val expectedIds = setOf(1L, 3L, 4L, 5L, 6L) + + assertEquals(expectedIds, result) + } + + @Test + fun `Do not return course ids that are not synced yet`() = runTest { + val entities = listOf( + CourseSyncSettingsEntity(1, "Course 1",true), + CourseSyncSettingsEntity(2, "Course 2",false), + CourseSyncSettingsEntity(3, "Course 3",false, fullFileSync = true), + CourseSyncSettingsEntity(4, "Course 4",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.ANNOUNCEMENTS_ID }), + CourseSyncSettingsEntity(5, "Course 5",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.DISCUSSIONS_ID }), + CourseSyncSettingsEntity(6, "Course 6",false, CourseSyncSettingsEntity.TABS.associateWith { it == Tab.PAGES_ID }), + ) + coEvery { courseSyncSettingsDao.findAll() } returns entities + coEvery { courseDao.findByIds(any()) } returns listOf(CourseEntity(Course(1)), CourseEntity(Course(3))) + + val result = repository.getSyncedCourseIds() + val expectedIds = setOf(1L, 3L) + + assertEquals(expectedIds, result) + } + + @Test + fun `Get courses from network if device is online`() = runTest { + val onlineCourses = listOf(listOf(Course(1), Course(2)), emptyList(), emptyList()) + val offlineCourses = listOf(listOf(Course(3), Course(4)), emptyList(), emptyList()) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCourses() } returns onlineCourses + coEvery { localDataSource.getCourses() } returns offlineCourses + + val courses = repository.getCourses() + + Assert.assertEquals(onlineCourses, courses) + } + + @Test + fun `Get courses from local database if device is offline`() = runTest { + val onlineCourses = listOf(listOf(Course(1), Course(2)), emptyList(), emptyList()) + val offlineCourses = listOf(listOf(Course(3), Course(4)), emptyList(), emptyList()) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getCourses() } returns onlineCourses + coEvery { localDataSource.getCourses() } returns offlineCourses + + val courses = repository.getCourses() + + Assert.assertEquals(offlineCourses, courses) + } + + @Test + fun `Get groups from network if device is online`() = runTest { + val onlineGroups = listOf(Group(1), Group(2)) + val offlineGroups = listOf(Group(3), Group(4)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getGroups() } returns onlineGroups + coEvery { localDataSource.getGroups() } returns offlineGroups + + val groups = repository.getGroups() + + Assert.assertEquals(onlineGroups, groups) + } + + @Test + fun `Get groups from local database if device is offline`() = runTest { + val onlineGroups = listOf(Group(1), Group(2)) + val offlineGroups = listOf(Group(3), Group(4)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getGroups() } returns onlineGroups + coEvery { localDataSource.getGroups() } returns offlineGroups + + val groups = repository.getGroups() + + Assert.assertEquals(offlineGroups, groups) + } } \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt new file mode 100644 index 0000000000..6671cff396 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardLocalDataSourceTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard.edit.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.daos.EditDashboardItemDao +import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity +import com.instructure.pandautils.room.offline.entities.EnrollmentState +import com.instructure.pandautils.room.offline.facade.CourseFacade +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class StudentEditDashboardLocalDataSourceTest { + + private val courseFacade: CourseFacade = mockk(relaxed = true) + private val editDashboardItemDao: EditDashboardItemDao = mockk(relaxed = true) + + private val dataSource = StudentEditDashboardLocalDataSource(courseFacade, editDashboardItemDao) + + @Test + fun `getCourses returns correct courses partitioned by enrollment state`() = runTest { + val fullCourse1 = Course(1, name = "Full Course 1", originalName = "Original name") + val fullCourse2 = Course(2, name = "Full Course 2", originalName = "Original name 2") + + coEvery { editDashboardItemDao.findByEnrollmentState(EnrollmentState.CURRENT) } returns + listOf(EditDashboardItemEntity(Course(1), EnrollmentState.CURRENT, 0)) + + coEvery { editDashboardItemDao.findByEnrollmentState(EnrollmentState.PAST) } returns + listOf(EditDashboardItemEntity(Course(2), EnrollmentState.PAST, 0)) + + coEvery { editDashboardItemDao.findByEnrollmentState(EnrollmentState.FUTURE) } returns + listOf(EditDashboardItemEntity(Course(3), EnrollmentState.FUTURE, 0)) + + coEvery { courseFacade.getCourseById(1) } returns fullCourse1 + coEvery { courseFacade.getCourseById(2) } returns fullCourse2 + coEvery { courseFacade.getCourseById(3) } returns null + + val result = dataSource.getCourses() + + assertEquals(3, result.flatten().size) + assertEquals(fullCourse1, result.flatten().first()) + assertEquals(fullCourse2, result.flatten()[1]) + assertEquals(Course(3), result.flatten()[2]) + } + + @Test + fun `getGroups returns empty list`() = runTest { + assertEquals(emptyList(), dataSource.getGroups()) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt new file mode 100644 index 0000000000..94af8b3cce --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/dashboard/edit/datasource/StudentEditDashboardNetwoekDataSourceTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.dashboard.edit.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class StudentEditDashboardNetworkDataSourceTest { + + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true) + + private val dataSource = StudentEditDashboardNetworkDataSource(courseApi, groupApi) + + @Test + fun `Get the correct courses when all requests are successful`() = runTest { + coEvery { courseApi.firstPageCoursesByEnrollmentState("active", any()) } returns + DataResult.Success(listOf(Course(1, name = "Course 1"))) + + coEvery { courseApi.firstPageCoursesByEnrollmentState("completed", any()) } returns + DataResult.Success(listOf(Course(2, name = "Course 2"))) + + coEvery { courseApi.firstPageCoursesByEnrollmentState("invited_or_pending", any()) } returns + DataResult.Success(listOf(Course(3, name = "Course 3"))) + + val result = dataSource.getCourses() + + assertEquals(3, result.flatten().size) + assertEquals("Course 1", result.flatten().first().name) + assertEquals("Course 2", result.flatten()[1].name) + assertEquals("Course 3", result.flatten()[2].name) + } + + @Test + fun `Do not show unpublished future courses`() = runTest { + coEvery { courseApi.firstPageCoursesByEnrollmentState("active", any()) } returns + DataResult.Success(listOf(Course(1, name = "Course 1"))) + + coEvery { courseApi.firstPageCoursesByEnrollmentState("completed", any()) } returns + DataResult.Success(listOf(Course(2, name = "Course 2"))) + + coEvery { courseApi.firstPageCoursesByEnrollmentState("invited_or_pending", any()) } returns + DataResult.Success(listOf(Course(3, name = "Course 3"), Course(4, name = "Course 4", workflowState = Course.WorkflowState.UNPUBLISHED))) + + val result = dataSource.getCourses() + + assertEquals(3, result.flatten().size) + assertEquals("Course 1", result.flatten().first().name) + assertEquals("Course 2", result.flatten()[1].name) + assertEquals("Course 3", result.flatten()[2].name) + } + + @Test(expected = IllegalStateException::class) + fun `Throw exception when at least one request fails`() = runTest { + coEvery { courseApi.firstPageCoursesByEnrollmentState("active", any()) } returns + DataResult.Success(listOf(Course(1, name = "Course 1"))) + + coEvery { courseApi.firstPageCoursesByEnrollmentState("completed", any()) } returns + DataResult.Success(listOf(Course(2, name = "Course 2"))) + + coEvery { courseApi.firstPageCoursesByEnrollmentState("invited_or_pending", any()) } returns + DataResult.Fail() + + dataSource.getCourses() + } + + @Test + fun `getGroups returns empty list if it's failed`() = runTest { + coEvery { groupApi.getFirstPageGroups(any()) } returns DataResult.Fail() + + val result = dataSource.getGroups() + + assertEquals(emptyList(), result) + } + + @Test + fun `getGroups returns correct groups`() = runTest { + val groups = listOf(Group(1), Group(2)) + coEvery { groupApi.getFirstPageGroups(any()) } returns DataResult.Success(groups) + + val result = dataSource.getGroups() + + assertEquals(groups, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt new file mode 100644 index 0000000000..06e07ceaa4 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/details/DiscussionDetailsRepositoryTest.kt @@ -0,0 +1,214 @@ +package com.instructure.student.features.discussion.details + +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.Failure +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsLocalDataSource +import com.instructure.student.features.discussion.details.datasource.DiscussionDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionDetailsRepositoryTest { + + private val networkDataSource: DiscussionDetailsNetworkDataSource = mockk(relaxed = true) + private val localDataSource: DiscussionDetailsLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = DiscussionDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Call markAsRead function when device is online`() = runTest { + val discussionEntryIds = listOf(1L, 2L, 3L) + + coEvery { networkDataSource.markAsRead(any(), any(), any()) } returns DataResult.Success(mockk()) + + val result = repository.markAsRead(mockk(), 1L, discussionEntryIds) + + assertEquals(discussionEntryIds, result) + } + + @Test + fun `Call deleteDiscussionEntry function when device is online`() = runTest { + + repository.deleteDiscussionEntry(mockk(), 1, 1) + coVerify(exactly = 1) { networkDataSource.deleteDiscussionEntry(any(), any(), any())} + } + + @Test + fun `Call deleteDiscussionEntry function when device is offline`() = runTest { + + repository.deleteDiscussionEntry(mockk(), 1, 1) + coVerify(exactly = 1) { networkDataSource.deleteDiscussionEntry(any(), any(), any())} + } + + @Test + fun `Call rateDiscussionEntry function when device is online`() = runTest { + + repository.rateDiscussionEntry(mockk(), 1, 1, 1) + coVerify(exactly = 1) { networkDataSource.rateDiscussionEntry(any(), any(), any(), any())} + } + + @Test + fun `Call rateDiscussionEntry function when device is offline`() = runTest { + + repository.rateDiscussionEntry(mockk(), 1, 1, 1) + coVerify(exactly = 1) { networkDataSource.rateDiscussionEntry(any(), any(), any(), any())} + } + + @Test + fun `Get getAuthenticatedSession function when device is online`() = runTest { + + val expectedResult: AuthenticatedSession = mockk() + coEvery { networkDataSource.getAuthenticatedSession("") } returns DataResult.Success(expectedResult) + val result = repository.getAuthenticatedSession("") + + assertEquals(expectedResult, result) + } + + @Test + fun `Get getAuthenticatedSession function when device is offline`() = runTest { + + coEvery { networkDataSource.getAuthenticatedSession(any()) } returns DataResult.Fail( + Failure.Exception(Exception(), "") + ) + + val result = repository.getAuthenticatedSession("") + + assertEquals(null, result) + } + + @Test + fun `Get course settings when device is online`() = runTest { + val localResult = CourseSettings(true, true) + val networkResult = CourseSettings(false, false) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCourseSettings(any(), any()) } returns DataResult.Success(networkResult) + coEvery { localDataSource.getCourseSettings(any(), any()) } returns DataResult.Success(localResult) + + val result = repository.getCourseSettings(1, true) + + assertEquals(networkResult, result) + } + + @Test + fun `Get course settings when device is offline`() = runTest { + val localResult = CourseSettings(true, true) + val networkResult = CourseSettings(false, false) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getCourseSettings(any(), any()) } returns DataResult.Success(networkResult) + coEvery { localDataSource.getCourseSettings(any(), any()) } returns DataResult.Success(localResult) + + val result = repository.getCourseSettings(1, true) + + assertEquals(localResult, result) + } + + @Test + fun `Get detailed discussion when device is online`() = runTest { + val localResult = DiscussionTopicHeader(1) + val networkResult = DiscussionTopicHeader(2) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getDetailedDiscussion(any(), any(), any()) } returns DataResult.Success(networkResult) + coEvery { localDataSource.getDetailedDiscussion(any(), any(), any()) } returns DataResult.Success(localResult) + + val result = repository.getDetailedDiscussion(mockk(), 1, true) + + assertEquals(networkResult, result) + } + + @Test + fun `Get detailed discussion when device is offline`() = runTest { + val localResult = DiscussionTopicHeader(1) + val networkResult = DiscussionTopicHeader(2) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getDetailedDiscussion(any(), any(), any()) } returns DataResult.Success(networkResult) + coEvery { localDataSource.getDetailedDiscussion(any(), any(), any()) } returns DataResult.Success(localResult) + + val result = repository.getDetailedDiscussion(mockk(), 1, true) + + assertEquals(localResult, result) + } + + @Test + fun `Get all group when device is online`() = runTest { + val localResult = listOf(Group(1), Group(3)) + val networkResult = listOf(Group(2), Group(4)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getFirstPageGroups(any(), any()) } returns DataResult.Success(networkResult) + coEvery { networkDataSource.getNextPageGroups(any(), any()) } returns DataResult.Success(emptyList()) + coEvery { localDataSource.getFirstPageGroups(any(), any()) } returns DataResult.Success(localResult) + coEvery { localDataSource.getNextPageGroups(any(), any()) } returns DataResult.Success(emptyList()) + + val result = repository.getAllGroups(1, true) + + assertEquals(networkResult, result) + } + + @Test + fun `Get all group when device is offline`() = runTest { + val localResult = listOf(Group(1), Group(3)) + val networkResult = listOf(Group(2), Group(4)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getFirstPageGroups(any(), any()) } returns DataResult.Success(networkResult) + coEvery { networkDataSource.getNextPageGroups(any(), any()) } returns DataResult.Success(emptyList()) + coEvery { localDataSource.getFirstPageGroups(any(), any()) } returns DataResult.Success(localResult) + coEvery { localDataSource.getNextPageGroups(any(), any()) } returns DataResult.Success(emptyList()) + + val result = repository.getAllGroups(1,true) + + assertEquals(localResult, result) + } + + @Test + fun `Get full discussion topic when device is online`() = runTest { + val localResult = DiscussionTopic(mutableListOf(1)) + val networkResult = DiscussionTopic(mutableListOf(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getFullDiscussionTopic(any(), any(), any()) } returns DataResult.Success(networkResult) + coEvery { localDataSource.getFullDiscussionTopic(any(), any(), any()) } returns DataResult.Success(localResult) + val result = repository.getFullDiscussionTopic(mockk(), 1, true) + + assertEquals(networkResult, result) + } + + @Test + fun `Get full discussion topic when device is offline`() = runTest { + val localResult = DiscussionTopic(mutableListOf(1)) + val networkResult = DiscussionTopic(mutableListOf(2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getFullDiscussionTopic(any(), any(), any()) } returns DataResult.Success(networkResult) + coEvery { localDataSource.getFullDiscussionTopic(any(), any(), any()) } returns DataResult.Success(localResult) + val result = repository.getFullDiscussionTopic(mockk(), 1, true) + + assertEquals(localResult, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..17989d2ab9 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsLocalDataSourceTest.kt @@ -0,0 +1,79 @@ +package com.instructure.student.features.discussion.details.datasource + +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.facade.DiscussionTopicFacade +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionDetailsLocalDataSourceTest { + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade = mockk(relaxed = true) + private val discussionTopicFacade: DiscussionTopicFacade = mockk(relaxed = true) + private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + private val groupFacade: GroupFacade = mockk(relaxed = true) + + private val dataSource = DiscussionDetailsLocalDataSource(discussionTopicHeaderFacade, discussionTopicFacade, courseSettingsDao, groupFacade) + + @Test + fun `Returns correct course settings`() = runTest { + val expectedCourseSettingsEntity = CourseSettingsEntity(CourseSettings(), 1) + + coEvery { courseSettingsDao.findByCourseId(any()) } returns expectedCourseSettingsEntity + + val result = dataSource.getCourseSettings(1, true).dataOrNull + + assertEquals(expectedCourseSettingsEntity.toApiModel(), result) + } + + @Test + fun `Returns detailed discussion`() = runTest { + val expectedDiscussionTopicHeader = DiscussionTopicHeader(1) + + coEvery { discussionTopicHeaderFacade.getDiscussionTopicHeaderById(any()) } returns expectedDiscussionTopicHeader + + val result = dataSource.getDetailedDiscussion(mockk(),1, true).dataOrNull + + assertEquals(expectedDiscussionTopicHeader, result) + } + + @Test + fun `Returns user groups first page`() = runTest { + val expectedGroups = listOf(Group(1), Group(2)) + + coEvery { groupFacade.getGroupsByUserId(any()) } returns expectedGroups + + val result = dataSource.getFirstPageGroups(1, true).dataOrNull + + assertEquals(expectedGroups, result) + } + + @Test + fun `User groups next page is always empty`() = runTest { + + val result = dataSource.getNextPageGroups("", true).dataOrNull + + assertEquals(emptyList(), result) + } + + @Test + fun `Get full discussion topic`() = runTest { + val expectedDiscussionTopic = DiscussionTopic() + + coEvery { discussionTopicFacade.getDiscussionTopic(any()) } returns expectedDiscussionTopic + + val result = dataSource.getFullDiscussionTopic(mockk(), 1, true).dataOrNull + + assertEquals(expectedDiscussionTopic, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..e63ffdf702 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/details/datasource/DiscussionDetailsNetworkDataSourceTest.kt @@ -0,0 +1,176 @@ +package com.instructure.student.features.discussion.details.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionDetailsNetworkDataSourceTest { + private val discussionApi: DiscussionAPI.DiscussionInterface = mockk(relaxed = true) + private val oAuthApi: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true) + + private val dataSource = DiscussionDetailsNetworkDataSource( + discussionApi = discussionApi, + oAuthApi = oAuthApi, + courseApi = courseApi, + groupApi = groupApi + ) + + @Test + fun `Call mark as read`() = runTest { + dataSource.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1) + + coVerify(exactly = 1) { discussionApi.markDiscussionTopicEntryRead(any(), any(), any(), any(), any()) } + } + + @Test + fun `Call delete discussion`() = runTest { + dataSource.deleteDiscussionEntry(CanvasContext.defaultCanvasContext(), 1, 1) + + coVerify(exactly = 1) { discussionApi.deleteDiscussionEntry(any(), any(), any(), any(), any()) } + } + + @Test + fun `Call rate discussion`() = runTest { + dataSource.rateDiscussionEntry(CanvasContext.defaultCanvasContext(), 1, 1, 1) + + coVerify(exactly = 1) { discussionApi.rateDiscussionEntry(any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Get authenticatedSession on successful call`() = runTest { + val expectedUrl = AuthenticatedSession("testUrl") + + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Success(expectedUrl) + + val result = dataSource.getAuthenticatedSession("").dataOrNull + + assertEquals(expectedUrl, result) + } + + @Test + fun `Get authenticatedSession on failed call`() = runTest { + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Fail(null, null) + + val result = dataSource.getAuthenticatedSession("").dataOrNull + + assertEquals(null, result) + } + + @Test + fun `Get course settings on successful call`() = runTest { + val expectedCourseSettings = CourseSettings() + + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Success(expectedCourseSettings) + + val result = dataSource.getCourseSettings(1, true).dataOrNull + + assertEquals(expectedCourseSettings, result) + } + + @Test + fun `Get course settings on failed call`() = runTest { + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Fail(null, null) + + val result = dataSource.getCourseSettings(1, true).dataOrNull + + assertEquals(null, result) + } + + @Test + fun `Get detailed discussion on successful call`() = runTest { + val expectedDiscussionTopicHeader = DiscussionTopicHeader() + + coEvery { discussionApi.getDetailedDiscussion(any(), any(), any(), any()) } returns DataResult.Success(expectedDiscussionTopicHeader) + + val result = dataSource.getDetailedDiscussion(CanvasContext.defaultCanvasContext(), 1, true).dataOrNull + + assertEquals(expectedDiscussionTopicHeader, result) + } + + @Test + fun `Get detailed discussion on failed call`() = runTest { + coEvery { discussionApi.getDetailedDiscussion(any(), any(), any(), any()) } returns DataResult.Fail(null, null) + + val result = dataSource.getDetailedDiscussion(CanvasContext.defaultCanvasContext(), 1, true).dataOrNull + + assertEquals(null, result) + } + + @Test + fun `Get groups first page on successful call`() = runTest { + val expectedGroupsFirstPage = listOf(Group(1), Group(2)) + + coEvery { groupApi.getFirstPageGroups(any()) } returns DataResult.Success(expectedGroupsFirstPage) + + val result = dataSource.getFirstPageGroups(1, true).dataOrNull + + assertEquals(expectedGroupsFirstPage, result) + } + + @Test + fun `Get groups first page on failed call`() = runTest { + coEvery { groupApi.getFirstPageGroups(any()) } returns DataResult.Fail(null, null) + + val result = dataSource.getFirstPageGroups(1, true).dataOrNull + + assertEquals(null, result) + } + + @Test + fun `Get groups next page on successful call`() = runTest { + val expectedGroupsNextPage = listOf(Group(1), Group(2)) + + coEvery { groupApi.getNextPageGroups(any(), any()) } returns DataResult.Success(expectedGroupsNextPage) + + val result = dataSource.getNextPageGroups("", true).dataOrNull + + assertEquals(expectedGroupsNextPage, result) + } + + @Test + fun `Get groups next page on failed call`() = runTest { + coEvery { groupApi.getNextPageGroups(any(), any()) } returns DataResult.Fail(null, null) + + val result = dataSource.getNextPageGroups("", true).dataOrNull + + assertEquals(null, result) + } + + @Test + fun `Get full discussion topic on successful call`() = runTest { + val expectedDiscussionTopic = DiscussionTopic() + + coEvery { discussionApi.getFullDiscussionTopic(any(), any(), any(), any(), any()) } returns DataResult.Success(expectedDiscussionTopic) + + val result = dataSource.getFullDiscussionTopic(CanvasContext.defaultCanvasContext(),1, true).dataOrNull + + assertEquals(expectedDiscussionTopic, result) + } + + @Test + fun `Get full discussion topic on failed call`() = runTest { + coEvery { discussionApi.getFullDiscussionTopic(any(), any(), any(), any(), any()) } returns DataResult.Fail(null, null) + + val result = dataSource.getFullDiscussionTopic(CanvasContext.defaultCanvasContext(),1, true).dataOrNull + + assertEquals(null, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt new file mode 100644 index 0000000000..b9274b200f --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/list/DiscussionListRepositoryTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list + +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.discussion.list.datasource.DiscussionListLocalDataSource +import com.instructure.student.features.discussion.list.datasource.DiscussionListNetworkDataSource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionListRepositoryTest { + + private val networkDataSource: DiscussionListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: DiscussionListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = DiscussionListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get announcement creation permission from network data source for course when device is online`() = runTest { + val course = Course(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getPermissionsForCourse(course) } returns CanvasContextPermission(canCreateAnnouncement = true) + coEvery { localDataSource.getPermissionsForCourse(course) } returns null + + val result = repository.getCreationPermission(course, true) + + Assert.assertTrue(result) + } + + @Test + fun `Get announcement creation permission from local data source for course when device is offline`() = runTest { + val course = Course(1) + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getPermissionsForCourse(course) } returns CanvasContextPermission(canCreateAnnouncement = true) + coEvery { localDataSource.getPermissionsForCourse(course) } returns null + + val result = repository.getCreationPermission(course, true) + + Assert.assertFalse(result) + } + + @Test + fun `Get announcement creation permission from network data source for group when device is online`() = runTest { + val group = Group(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getPermissionsForGroup(group) } returns CanvasContextPermission(canCreateAnnouncement = true) + coEvery { localDataSource.getPermissionsForGroup(group) } returns null + + val result = repository.getCreationPermission(group, true) + + Assert.assertTrue(result) + } + + @Test + fun `Get discussion creation permission from network data source for course when device is online`() = runTest { + val course = Course(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getPermissionsForCourse(course) } returns CanvasContextPermission(canCreateDiscussionTopic = true) + coEvery { localDataSource.getPermissionsForCourse(course) } returns null + + val result = repository.getCreationPermission(course, false) + + Assert.assertTrue(result) + } + + @Test + fun `Get discussion creation permission from local data source for course when device is offline`() = runTest { + val course = Course(1) + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getPermissionsForCourse(course) } returns CanvasContextPermission(canCreateDiscussionTopic = true) + coEvery { localDataSource.getPermissionsForCourse(course) } returns null + + val result = repository.getCreationPermission(course, false) + + Assert.assertFalse(result) + } + + @Test + fun `Get discussion creation permission from network data source for group when device is online`() = runTest { + val group = Group(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getPermissionsForGroup(group) } returns CanvasContextPermission(canCreateDiscussionTopic = true) + coEvery { localDataSource.getPermissionsForGroup(group) } returns null + + val result = repository.getCreationPermission(group, false) + + Assert.assertTrue(result) + } + + @Test + fun `Get discussions from network when device is online`() = runTest { + val networkDiscussions = listOf(DiscussionTopicHeader(id = 1, title = "Discuss"), DiscussionTopicHeader(id = 1, title = "Discuss 2")) + val localDiscussions = listOf(DiscussionTopicHeader(id = 3, title = "Discuss 3"), DiscussionTopicHeader(id = 4, title = "Discuss 4")) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getDiscussions(any(), any()) } returns networkDiscussions + coEvery { localDataSource.getDiscussions(any(), any()) } returns localDiscussions + + val result = repository.getDiscussionTopicHeaders(Course(1), false, true) + + Assert.assertEquals(networkDiscussions, result) + } + + @Test + fun `Get discussions from local store when device is offline`() = runTest { + val networkDiscussions = listOf(DiscussionTopicHeader(id = 1, title = "Discuss"), DiscussionTopicHeader(id = 1, title = "Discuss 2")) + val localDiscussions = listOf(DiscussionTopicHeader(id = 3, title = "Discuss 3"), DiscussionTopicHeader(id = 4, title = "Discuss 4")) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getDiscussions(any(), any()) } returns networkDiscussions + coEvery { localDataSource.getDiscussions(any(), any()) } returns localDiscussions + + val result = repository.getDiscussionTopicHeaders(Course(1), false, true) + + Assert.assertEquals(localDiscussions, result) + } + + @Test + fun `Get announcements from network when device is online`() = runTest { + val networkAnnouncements = listOf(DiscussionTopicHeader(id = 1, title = "Announce"), DiscussionTopicHeader(id = 1, title = "Announce 2")) + val localAnnouncements = listOf(DiscussionTopicHeader(id = 3, title = "Announce 3"), DiscussionTopicHeader(id = 4, title = "Announce 4")) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAnnouncements(any(), any()) } returns networkAnnouncements + coEvery { localDataSource.getAnnouncements(any(), any()) } returns localAnnouncements + + val result = repository.getDiscussionTopicHeaders(Course(1), true, true) + + Assert.assertEquals(networkAnnouncements, result) + } + + @Test + fun `Get announcements from local store when device is offline`() = runTest { + val networkAnnouncements = listOf(DiscussionTopicHeader(id = 1, title = "Announce"), DiscussionTopicHeader(id = 1, title = "Announce 2")) + val localAnnouncements = listOf(DiscussionTopicHeader(id = 3, title = "Announce 3"), DiscussionTopicHeader(id = 4, title = "Announce 4")) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAnnouncements(any(), any()) } returns networkAnnouncements + coEvery { localDataSource.getAnnouncements(any(), any()) } returns localAnnouncements + + val result = repository.getDiscussionTopicHeaders(Course(1), true, true) + + Assert.assertEquals(networkAnnouncements, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt new file mode 100644 index 0000000000..61c6cdba9e --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListLocalDataSourceTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionListLocalDataSourceTest { + + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade = mockk(relaxed = true) + + private val dataSource = DiscussionListLocalDataSource(discussionTopicHeaderFacade) + + @Test + fun `Return null for course permission`() = runTest { + Assert.assertNull(dataSource.getPermissionsForCourse(Course(1))) + } + + @Test + fun `Return null for groups permission`() = runTest { + Assert.assertNull(dataSource.getPermissionsForGroup(Group(1))) + } + + @Test + fun `Datasource returns correct discussions for course`() = runTest { + val discussions = listOf(DiscussionTopicHeader(id = 1, "Discuss"), DiscussionTopicHeader(id = 2, "Discuss 2")) + coEvery { discussionTopicHeaderFacade.getDiscussionsForCourse(any()) } returns discussions + + val result = dataSource.getDiscussions(Course(1), false) + + Assert.assertEquals(discussions, result) + } + + @Test + fun `Datasource returns correct announcements for course`() = runTest { + val announcements = listOf(DiscussionTopicHeader(id = 1, "announce"), DiscussionTopicHeader(id = 2, "announce 2")) + coEvery { discussionTopicHeaderFacade.getAnnouncementsForCourse(any()) } returns announcements + + val result = dataSource.getAnnouncements(Course(1), false) + + Assert.assertEquals(announcements, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..8037c306b8 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/list/datasource/DiscussionListNetworkDataSourceTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.discussion.list.datasource + +import com.instructure.canvasapi2.apis.AnnouncementAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionListNetworkDataSourceTest { + + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true) + private val discussionApi: DiscussionAPI.DiscussionInterface = mockk(relaxed = true) + private val announcementApi: AnnouncementAPI.AnnouncementInterface = mockk(relaxed = true) + + private val discussionListNetworkDataSource = DiscussionListNetworkDataSource( + courseApi = courseApi, + groupApi = groupApi, + discussionApi = discussionApi, + announcementApi = announcementApi + ) + + @Test + fun `Return null permissions for course when the request fails`() = runTest { + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Fail() + + val result = discussionListNetworkDataSource.getPermissionsForCourse(Course(1)) + + Assert.assertNull(result) + } + + @Test + fun `Return correct course permissions when the request is successful`() = runTest { + val permissions = CanvasContextPermission(canCreateAnnouncement = true) + val course = Course(1).apply { this.permissions = permissions } + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Success(course) + + val result = discussionListNetworkDataSource.getPermissionsForCourse(Course(1)) + + Assert.assertEquals(permissions, result) + } + + @Test + fun `Return null permissions for group when the request fails`() = runTest { + coEvery { groupApi.getDetailedGroup(any(), any()) } returns DataResult.Fail() + + val result = discussionListNetworkDataSource.getPermissionsForGroup(Group(1)) + + Assert.assertNull(result) + } + + @Test + fun `Return correct group permissions when the request is successful`() = runTest { + val permissions = CanvasContextPermission(canCreateAnnouncement = true) + val group = Group(1).apply { this.permissions = permissions } + coEvery { groupApi.getDetailedGroup(any(), any()) } returns DataResult.Success(group) + + val result = discussionListNetworkDataSource.getPermissionsForGroup(Group(1)) + + Assert.assertEquals(permissions, result) + } + + @Test(expected = IllegalStateException::class) + fun `Throw error when discussion request fails`() = runTest { + coEvery { discussionApi.getFirstPageDiscussionTopicHeaders(any(), any(), any()) } returns DataResult.Fail() + + discussionListNetworkDataSource.getDiscussions(Course(1), false) + } + + @Test + fun `Return empty list when discussion request is successful but no discussions are returned`() = runTest { + coEvery { discussionApi.getFirstPageDiscussionTopicHeaders(any(), any(), any()) } returns DataResult.Success(emptyList()) + + val result = discussionListNetworkDataSource.getDiscussions(Course(1), false) + + Assert.assertTrue(result.isEmpty()) + } + + @Test + fun `Return correct list of discussions when discussion request is successful`() = runTest { + val discussions = listOf(DiscussionTopicHeader(id = 1, title = "Discussion 1")) + coEvery { discussionApi.getFirstPageDiscussionTopicHeaders(any(), any(), any()) } returns DataResult.Success(discussions) + + val result = discussionListNetworkDataSource.getDiscussions(Course(1), false) + + Assert.assertEquals(discussions, result) + } + + @Test(expected = IllegalStateException::class) + fun `Throw error when announcement request fails`() = runTest { + coEvery { announcementApi.getFirstPageAnnouncementsList(any(), any(), any()) } returns DataResult.Fail() + + discussionListNetworkDataSource.getAnnouncements(Course(1), false) + } + + @Test + fun `Return empty list when announcement request is successful but no discussions are returned`() = runTest { + coEvery { announcementApi.getFirstPageAnnouncementsList(any(), any(), any()) } returns DataResult.Success(emptyList()) + + val result = discussionListNetworkDataSource.getAnnouncements(Course(1), false) + + Assert.assertTrue(result.isEmpty()) + } + + @Test + fun `Return correct list of announcements when discussion request is successful`() = runTest { + val announcements = listOf(DiscussionTopicHeader(id = 1, title = "Announce 1")) + coEvery { announcementApi.getFirstPageAnnouncementsList(any(), any(), any()) } returns DataResult.Success(announcements) + + val result = discussionListNetworkDataSource.getAnnouncements(Course(1), false) + + Assert.assertEquals(announcements, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt new file mode 100644 index 0000000000..71e3db5f29 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt @@ -0,0 +1,112 @@ +package com.instructure.student.features.discussion.routing + +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperLocalDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionRouteHelperStudentRepositoryTest { + + private val networkDataSource: DiscussionRouteHelperNetworkDataSource = mockk(relaxed = true) + private val localDataSource: DiscussionRouteHelperLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = DiscussionRouteHelperStudentRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Call getEnabledFeaturesForCourse function when device is online`() = runTest { + val expected = true + + coEvery { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getEnabledFeaturesForCourse(any(), any()) } returns expected + + val result = repository.getEnabledFeaturesForCourse(mockk(), false) + + assertEquals(expected, result) + } + + @Test + fun `Call getEnabledFeaturesForCourse function when device is offline`() = runTest { + val expected = true + + coEvery { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getEnabledFeaturesForCourse(any(), any()) } returns expected + + val result = repository.getEnabledFeaturesForCourse(mockk(), false) + + assertEquals(expected, result) + } + + @Test + fun `Call getDiscussionTopicHeader function when device is online`() = runTest { + val onlineExpected = DiscussionTopicHeader(1L) + val offlineExpected = DiscussionTopicHeader(2L) + + coEvery { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getDiscussionTopicHeader(any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getDiscussionTopicHeader(any(), any(), any()) } returns offlineExpected + + val result = repository.getDiscussionTopicHeader(mockk(), 1, true) + + assertEquals(onlineExpected, result) + } + + @Test + fun `Call getDiscussionTopicHeader function when device is offline`() = runTest { + val onlineExpected = DiscussionTopicHeader(1L) + val offlineExpected = DiscussionTopicHeader(2L) + + coEvery { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getDiscussionTopicHeader(any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getDiscussionTopicHeader(any(), any(), any()) } returns offlineExpected + + val result = repository.getDiscussionTopicHeader(mockk(), 1, true) + + assertEquals(offlineExpected, result) + } + + @Test + fun `Call getAllGroups function when device is online`() = runTest { + val onlineExpected = listOf(Group(1L)) + val offlineExpected = listOf(Group(2L)) + + coEvery { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAllGroups(any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getAllGroups(any(), any(), any()) } returns offlineExpected + + val result = repository.getAllGroups(mockk(), 1L, true) + + assertEquals(onlineExpected, result) + } + + @Test + fun `Call getAllGroups function when device is offline`() = runTest { + val onlineExpected = listOf(Group(1L)) + val offlineExpected = listOf(Group(2L)) + + coEvery { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getAllGroups(any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getAllGroups(any(), any(), any()) } returns offlineExpected + + val result = repository.getAllGroups(mockk(), 1L, true) + + assertEquals(offlineExpected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..792bc1d265 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsLocalDataSourceTest.kt @@ -0,0 +1,75 @@ +package com.instructure.student.features.file.details + +import android.webkit.URLUtil +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import com.instructure.student.features.files.details.FileDetailsLocalDataSource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@ExperimentalCoroutinesApi +class FileDetailsLocalDataSourceTest { + + private val fileFolderDao: FileFolderDao = mockk(relaxed = true) + private val localFileDao: LocalFileDao = mockk(relaxed = true) + + private val fileDetailsLocalDataSource = FileDetailsLocalDataSource( + fileFolderDao, + localFileDao + ) + + @Before + fun setup() { + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `getFileFolderFromURL returns api model`() = runTest { + val expected: List = listOf( + FileFolder(id = 1, name = "File 1", url = "localFile.path.1"), + FileFolder(id = 2, name = "File 2", url = "localFile.path.2"), + FileFolder(id = 3, name = "File 3", url = "localFile.path.3") + ) + + val localFiles: List = listOf( + LocalFileEntity(id = 1, courseId = 1, createdDate = Date(), path = "localFile.path.1"), + LocalFileEntity(id = 2, courseId = 1, createdDate = Date(), path = "localFile.path.2"), + LocalFileEntity(id = 3, courseId = 1, createdDate = Date(), path = "localFile.path.3"), + ) + + coEvery { localFileDao.findById(2) } returns localFiles[1] + coEvery { fileFolderDao.findById(2) } returns FileFolderEntity(expected[1]) + + val fileFolder = fileDetailsLocalDataSource.getFileFolderFromURL("https://www.instructure.com", 2, false) + + assertEquals(expected[1], fileFolder) + } + + @Test + fun `getFileFolderFromURL returns null if not exists`() = runTest { + coEvery { localFileDao.findById(5) } returns null + + val fileFolder = fileDetailsLocalDataSource.getFileFolderFromURL("https://www.instructure.com", 5, false) + + assertEquals(null, fileFolder) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..ce9cee3051 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsNetworkDataSourceTest.kt @@ -0,0 +1,85 @@ +package com.instructure.student.features.file.details + +import android.webkit.URLUtil +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.files.details.FileDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FileDetailsNetworkDataSourceTest { + + private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true) + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true) + + private val fileListNetworkDataSource = FileDetailsNetworkDataSource(moduleApi, fileFolderApi) + + @Before + fun setup() { + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `getFileFolderFromURL returns api model`() = runTest{ + val expected: List = listOf( + FileFolder(id = 1, name = "File 1", url = "localFile.path.1"), + FileFolder(id = 2, name = "File 2", url = "localFile.path.2"), + FileFolder(id = 3, name = "File 3", url = "localFile.path.3") + ) + + coEvery { fileFolderApi.getFileFolderFromURL(any(), any()) } returns DataResult.Success(expected[0]) + + val result = fileListNetworkDataSource.getFileFolderFromURL("url", 1, true) + + assertEquals(expected[0], result) + } + + @Test + fun `getFileFolderFromURL returns null on error`() = runTest { + coEvery { fileFolderApi.getFileFolderFromURL(any(), any()) } returns DataResult.Fail() + + val result = fileListNetworkDataSource.getFileFolderFromURL("url", 1, true) + + assertEquals(null, result) + } + + @Test + fun `markAsRead returns if successful`() = runTest { + val expected = "".toResponseBody(null) + + coEvery { moduleApi.markModuleItemRead(any(), any(), any(), any(), any()) } returns DataResult.Success(expected) + + val result = fileListNetworkDataSource.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1, true) + + assertEquals(expected, result) + } + + @Test + fun `markAsRead returns null on error`() = runTest { + coEvery { moduleApi.markModuleItemRead(any(), any(), any(), any(), any()) } returns DataResult.Fail() + + val result = fileListNetworkDataSource.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1, true) + + assertEquals(null, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt new file mode 100644 index 0000000000..517b2d509b --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/details/FileDetailsRepositoryTest.kt @@ -0,0 +1,76 @@ +package com.instructure.student.features.file.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.files.details.FileDetailsLocalDataSource +import com.instructure.student.features.files.details.FileDetailsNetworkDataSource +import com.instructure.student.features.files.details.FileDetailsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FileDetailsRepositoryTest { + + private val fileDetailsLocalDataSource: FileDetailsLocalDataSource = mockk(relaxed = true) + private val fileDetailsNetworkDataSource: FileDetailsNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val fileDetailsRepository = FileDetailsRepository( + fileDetailsLocalDataSource, + fileDetailsNetworkDataSource, + networkStateProvider, + featureFlagProvider + ) + + @Before + fun setup() { + coEvery { featureFlagProvider.offlineEnabled() } returns true + coEvery { networkStateProvider.isOnline() } returns true + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `getFileFolderFromURL calls localDataSource when offline`() = runTest { + coEvery { networkStateProvider.isOnline() } returns false + fileDetailsRepository.getFileFolderFromURL("https://instructure.com", 1, false) + + coVerify { fileDetailsLocalDataSource.getFileFolderFromURL("https://instructure.com", 1, false) } + } + + @Test + fun `getFileFolderFromURL calls networkDataSource when online`() = runTest { + coEvery { networkStateProvider.isOnline() } returns true + fileDetailsRepository.getFileFolderFromURL("https://instructure.com", 1, false) + + coVerify { fileDetailsNetworkDataSource.getFileFolderFromURL("https://instructure.com", 1, false) } + } + + @Test + fun `markAsRead calls networkDataSource when offline`() = runTest { + coEvery { networkStateProvider.isOnline() } returns false + fileDetailsRepository.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1, false) + + coVerify { fileDetailsNetworkDataSource.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1, false) } + } + + @Test + fun `markAsRead calls networkDataSource when online`() = runTest { + coEvery { networkStateProvider.isOnline() } returns true + fileDetailsRepository.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1, false) + + coVerify { fileDetailsNetworkDataSource.markAsRead(CanvasContext.defaultCanvasContext(), 1, 1, false) } + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt new file mode 100644 index 0000000000..7d6eeecf51 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListLocalDataSourceTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.file.list + +import android.webkit.URLUtil +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import com.instructure.student.features.files.list.FileListLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.Date + +@ExperimentalCoroutinesApi +class FileListLocalDataSourceTest { + + private val fileFolderDao: FileFolderDao = mockk(relaxed = true) + private val localFileDao: LocalFileDao = mockk(relaxed = true) + + private val fileListLocalDataSource = FileListLocalDataSource( + fileFolderDao, + localFileDao + ) + + @Before + fun setup() { + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `getFolders returns api models`() = runTest { + val expected = listOf( + FileFolder(id = 1, name = "Folder 1", parentFolderId = 0), + FileFolder(id = 2, name = "Folder 2", parentFolderId = 0), + FileFolder(id = 3, name = "Folder 3", parentFolderId = 0) + ) + coEvery { fileFolderDao.findVisibleFoldersByParentId(any()) } returns expected.map { FileFolderEntity(it) } + + val result = fileListLocalDataSource.getFolders(0, true) + + coVerify { + fileFolderDao.findVisibleFoldersByParentId(0) + } + + assertEquals(DataResult.Success(expected), result) + } + + @Test + fun `getFiles replaces url with path`() = runTest { + val files = listOf( + FileFolder(id = 1, name = "File 1", url = "url_1", thumbnailUrl = "thumbnail_url_1"), + FileFolder(id = 2, name = "File 2", url = "url_2", thumbnailUrl = "thumbnail_url_2"), + FileFolder(id = 3, name = "File 3", url = "url_3", thumbnailUrl = "thumbnail_url_3") + ) + + val localFiles = listOf( + LocalFileEntity(id = 1, courseId = 1, createdDate = Date(), path = "path_1"), + LocalFileEntity(id = 2, courseId = 1, createdDate = Date(), path = "path_2"), + LocalFileEntity(id = 3, courseId = 1, createdDate = Date(), path = "path_3") + ) + + coEvery { localFileDao.findByIds(any()) } returns localFiles + coEvery { fileFolderDao.findVisibleFilesByFolderId(any()) } returns files.map { FileFolderEntity(it) } + + val expected = files.map { it.copy(url = "path_${it.id}", thumbnailUrl = null) } + val result = fileListLocalDataSource.getFiles(0, true) + + coVerify { + localFileDao.findByIds(listOf(1, 2, 3)) + fileFolderDao.findVisibleFilesByFolderId(0) + } + + assertEquals(DataResult.Success(expected), result) + } + + @Test + fun `getFolder returns api model`() = runTest { + coEvery { fileFolderDao.findById(any()) } returns FileFolderEntity(FileFolder(id = 1, name = "Folder 1", parentFolderId = 0)) + + val result = fileListLocalDataSource.getFolder(0, true) + + coVerify { + fileFolderDao.findById(0) + } + + assertEquals(FileFolder(id = 1, name = "Folder 1", parentFolderId = 0), result) + } + + @Test + fun `getFolder returns null if not exists`() = runTest { + coEvery { fileFolderDao.findById(any()) } returns null + + val result = fileListLocalDataSource.getFolder(0, true) + + coVerify { + fileFolderDao.findById(0) + } + + assertNull(result) + } + + @Test + fun `getRootFolderForContext returns api model`() = runTest { + coEvery { fileFolderDao.findRootFolderForContext(any()) } returns FileFolderEntity(FileFolder(id = 1, name = "Folder 1", parentFolderId = 0)) + + val result = fileListLocalDataSource.getRootFolderForContext(CanvasContext.defaultCanvasContext(), true) + + assertEquals(FileFolder(id = 1, name = "Folder 1", parentFolderId = 0), result) + } + + @Test + fun `getRootFolderForContext returns null if not exists`() = runTest { + coEvery { fileFolderDao.findRootFolderForContext(any()) } returns null + + val result = fileListLocalDataSource.getRootFolderForContext(CanvasContext.defaultCanvasContext(), true) + + assertNull(result) + } + + @Test + fun `getNextPage always returns empty`() = runTest { + val result = fileListLocalDataSource.getNextPage("url", true) + + assertEquals(DataResult.Success(emptyList()), result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..575e20b7da --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListNetworkDataSourceTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.file.list + +import android.webkit.URLUtil +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.files.list.FileListNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FileListNetworkDataSourceTest { + + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface = mockk(relaxed = true) + + private val fileListNetworkDataSource = FileListNetworkDataSource(fileFolderApi) + + @Before + fun setup() { + mockkStatic(URLUtil::class) + every { URLUtil.isNetworkUrl(any()) } returns true + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `getFolders() calls with correct params`() = runTest { + val expected = listOf( + FileFolder(id = 1, name = "Folder 1", parentFolderId = 0), + FileFolder(id = 2, name = "Folder 2", parentFolderId = 0), + FileFolder(id = 3, name = "Folder 3", parentFolderId = 0) + ) + + coEvery { fileFolderApi.getFirstPageFolders(any(), any()) } returns DataResult.Success(expected) + + val actual = fileListNetworkDataSource.getFolders(0, true) + + coVerify { + fileFolderApi.getFirstPageFolders( + 0, + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + ) + } + + assertEquals(DataResult.Success(expected), actual) + } + + @Test + fun `getFiles() calls with correct params`() = runTest { + val expected = listOf( + FileFolder(id = 1, name = "File 1", folderId = 0), + FileFolder(id = 2, name = "File 2", folderId = 0), + FileFolder(id = 3, name = "File 3", folderId = 0) + ) + + coEvery { fileFolderApi.getFirstPageFiles(any(), any()) } returns DataResult.Success(expected) + + val actual = fileListNetworkDataSource.getFiles(0, true) + + coVerify { + fileFolderApi.getFirstPageFiles( + 0, + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + ) + } + + assertEquals(DataResult.Success(expected), actual) + } + + @Test + fun `getFolder calls with correct params`() = runTest { + val expected = FileFolder(id = 1, name = "Folder 1", parentFolderId = 0) + + coEvery { fileFolderApi.getFolder(any(), any()) } returns DataResult.Success(expected) + + val actual = fileListNetworkDataSource.getFolder(1, true) + + coVerify { fileFolderApi.getFolder(1, RestParams(isForceReadFromNetwork = true)) } + + assertEquals(expected, actual) + } + + @Test + fun `getFolder() returns null on error`() = runTest { + coEvery { fileFolderApi.getFolder(any(), any()) } returns DataResult.Fail() + + val actual = fileListNetworkDataSource.getFolder(1, true) + + assertNull(actual) + } + + @Test + fun `getRootFolderForContext() calls with correct params`() = runTest { + val expected = FileFolder(id = 1, name = "Root", parentFolderId = 0) + + coEvery { fileFolderApi.getRootFolderForContext(any(), any(), any()) } returns DataResult.Success(expected) + + val actual = fileListNetworkDataSource.getRootFolderForContext(CanvasContext.emptyCourseContext(1L), true) + + coVerify { + fileFolderApi.getRootFolderForContext( + 1, + CanvasContext.Type.COURSE.apiString, + RestParams(isForceReadFromNetwork = true) + ) + } + + assertEquals(expected, actual) + } + + @Test + fun `getRootFolderForContext() returns null on error`() = runTest { + coEvery { fileFolderApi.getRootFolderForContext(any(), any(), any()) } returns DataResult.Fail() + + val actual = fileListNetworkDataSource.getRootFolderForContext(CanvasContext.emptyCourseContext(1L), true) + + assertNull(actual) + } + + @Test + fun `getNextPage() calls with correct params`() = runTest { + val expected = listOf( + FileFolder(id = 1, name = "File 1", folderId = 0), + FileFolder(id = 2, name = "File 2", folderId = 0), + FileFolder(id = 3, name = "File 3", folderId = 0) + ) + + coEvery { fileFolderApi.getNextPageFileFoldersList(any(), any()) } returns DataResult.Success(expected) + + val actual = fileListNetworkDataSource.getNextPage("next_url", true) + + coVerify { + fileFolderApi.getNextPageFileFoldersList( + "next_url", + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + ) + } + + assertEquals(DataResult.Success(expected), actual) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt new file mode 100644 index 0000000000..79b296608f --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/file/list/FileListRepositoryTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.features.file.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.files.list.FileListLocalDataSource +import com.instructure.student.features.files.list.FileListNetworkDataSource +import com.instructure.student.features.files.list.FileListRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FileListRepositoryTest { + + private val fileListLocalDataSource: FileListLocalDataSource = mockk(relaxed = true) + private val fileListNetworkDataSource: FileListNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val fileListRepository = FileListRepository( + fileListLocalDataSource, + fileListNetworkDataSource, + networkStateProvider, + featureFlagProvider + ) + + @Before + fun setup() { + coEvery { featureFlagProvider.offlineEnabled() } returns true + coEvery { networkStateProvider.isOnline() } returns true + } + + @Test + fun `use localDataSource when network is offline`() = runTest { + coEvery { networkStateProvider.isOnline() } returns false + + assert(fileListRepository.dataSource() is FileListLocalDataSource) + } + + @Test + fun `use networkDataSource when network is online`() = runTest { + coEvery { networkStateProvider.isOnline() } returns true + + assert(fileListRepository.dataSource() is FileListNetworkDataSource) + } + + @Test + fun `Return first page folders if multiple pages`() = runTest { + val firstPage = listOf(mockk()) + + coEvery { fileListNetworkDataSource.getFolders(any(), any()) } returns DataResult.Success(firstPage, linkHeaders = LinkHeaders(nextUrl = "nextPage")) + + val result = fileListRepository.getFirstPageItems(1, false) + + assertEquals(firstPage, result.dataOrNull) + } + + @Test + fun `getFirstPageItems() concats first page files to the last page folders`() = runTest { + val folders = listOf(mockk()) + val files = listOf(mockk()) + + coEvery { fileListNetworkDataSource.getFolders(any(), any()) } returns DataResult.Success(folders) + coEvery { fileListNetworkDataSource.getFiles(any(), any()) } returns DataResult.Success(files, linkHeaders = LinkHeaders(nextUrl = "nextPage")) + + val result = fileListRepository.getFirstPageItems(1, false) + + assertEquals(DataResult.Success(folders + files, linkHeaders = LinkHeaders(nextUrl = "nextPage")), result) + } + + + @Test + fun `getNextPage() returns only next page if not last`() = runTest { + val folders = listOf(mockk()) + val nextNext = listOf(mockk()) + + coEvery { fileListNetworkDataSource.getNextPage("nextPage", any()) } returns DataResult.Success(folders, linkHeaders = LinkHeaders(nextUrl = "nextNextPage")) + coEvery { fileListNetworkDataSource.getNextPage("nextNextPage", any()) } returns DataResult.Success(nextNext) + + val result = fileListRepository.getNextPage("nextPage", 0L,false) + + assertEquals(DataResult.Success(folders, linkHeaders = LinkHeaders(nextUrl = "nextNextPage")), result) + } + + @Test + fun `getNextPage() concats first page files to last page folders`() = runTest { + val folders = listOf(mockk()) + val files = listOf(mockk()) + + coEvery { fileListNetworkDataSource.getNextPage("nextPage", any()) } returns DataResult.Success(folders) + coEvery { fileListNetworkDataSource.getFiles(any(), any()) } returns DataResult.Success(files, linkHeaders = LinkHeaders(nextUrl = "nextNextPage")) + + val result = fileListRepository.getNextPage("nextPage", 0L,false) + + assertEquals(DataResult.Success(folders + files, linkHeaders = LinkHeaders(nextUrl = "nextNextPage")), result) + } + + @Test + fun `getFirstPageItems() returns fail if first call fails`() = runTest { + coEvery { fileListNetworkDataSource.getFolders(any(), any()) } returns DataResult.Fail() + + assertEquals(DataResult.Fail(), fileListRepository.getFirstPageItems(1, false)) + } + + @Test + fun `getFirstPageItems() returns fail if next page fails`() = runTest { + val firstPage = listOf(mockk()) + + coEvery { fileListNetworkDataSource.getFolders(any(), any()) } returns DataResult.Success(firstPage) + coEvery { fileListNetworkDataSource.getFiles(any(), any()) } returns DataResult.Fail() + + assertEquals(DataResult.Fail(), fileListRepository.getFirstPageItems(1, false)) + } + + @Test + fun `getNextPage() returns fail if the first page fails`() = runTest { + coEvery { fileListNetworkDataSource.getNextPage("nextPage", any()) } returns DataResult.Fail() + + assertEquals(DataResult.Fail(), fileListRepository.getNextPage("nextPage", 0L,false)) + } + + @Test + fun `getNextPage() returns fail if the next page fails`() = runTest { + val folders = listOf(mockk()) + + coEvery { fileListNetworkDataSource.getNextPage("nextPage", any()) } returns DataResult.Success(folders) + coEvery { fileListNetworkDataSource.getFiles(any(), any()) } returns DataResult.Fail() + + assertEquals(DataResult.Fail(), fileListRepository.getNextPage("nextPage", 0L,false)) + } + + @Test + fun `getFolder() calls localDataSource when offline`() = runTest { + coEvery { networkStateProvider.isOnline() } returns false + fileListRepository.getFolder(1, false) + + coVerify { fileListLocalDataSource.getFolder(1, false) } + } + + @Test + fun `getFolder() calls networkDataSource when online`() = runTest { + coEvery { networkStateProvider.isOnline() } returns true + fileListRepository.getFolder(1, false) + + coVerify { fileListNetworkDataSource.getFolder(1, false) } + } + + @Test + fun `getRootFolderForContext() calls localDataSource when offline`() = runTest { + coEvery { networkStateProvider.isOnline() } returns false + fileListRepository.getRootFolderForContext(CanvasContext.emptyCourseContext(1L), false) + + coVerify { fileListLocalDataSource.getRootFolderForContext(CanvasContext.emptyCourseContext(1L), false) } + } + + @Test + fun `getRootFolderForContext() calls networkDataSource when online`() = runTest { + coEvery { networkStateProvider.isOnline() } returns true + fileListRepository.getRootFolderForContext(CanvasContext.emptyCourseContext(1L), false) + + coVerify { fileListNetworkDataSource.getRootFolderForContext(CanvasContext.emptyCourseContext(1L), false) } + } + +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt new file mode 100644 index 0000000000..ca0ade55d9 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/grades/GradesListRepositoryTest.kt @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades + +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.grades.datasource.GradesListLocalDataSource +import com.instructure.student.features.grades.datasource.GradesListNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class GradesListRepositoryTest { + + private val networkDataSource: GradesListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: GradesListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = GradesListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get course with grade if device is online`() = runTest { + val onlineExpected = Course(1) + val offlineExpected = Course(2) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCourseWithGrade(any(), any()) } returns onlineExpected + coEvery { localDataSource.getCourseWithGrade(any(), any()) } returns offlineExpected + + val result = repository.getCourseWithGrade(1, true) + + coVerify { networkDataSource.getCourseWithGrade(1, true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get course with grade if device is offline`() = runTest { + val onlineExpected = Course(1) + val offlineExpected = Course(2) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getCourseWithGrade(any(), any()) } returns onlineExpected + coEvery { localDataSource.getCourseWithGrade(any(), any()) } returns offlineExpected + + val result = repository.getCourseWithGrade(1, true) + + coVerify { localDataSource.getCourseWithGrade(1, true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get observee enrollments if device is online`() = runTest { + val onlineExpected = listOf(Enrollment(1)) + val offlineExpected = listOf(Enrollment(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getObserveeEnrollments(any()) } returns onlineExpected + coEvery { localDataSource.getObserveeEnrollments(any()) } returns offlineExpected + + val result = repository.getObserveeEnrollments(true) + + coVerify { networkDataSource.getObserveeEnrollments(true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get observee enrollments if device is offline`() = runTest { + val onlineExpected = listOf(Enrollment(1)) + val offlineExpected = listOf(Enrollment(2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getObserveeEnrollments(any()) } returns onlineExpected + coEvery { localDataSource.getObserveeEnrollments(any()) } returns offlineExpected + + val result = repository.getObserveeEnrollments(true) + + coVerify { localDataSource.getObserveeEnrollments(true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get assignment groups with assignments for grading period if device is online`() = runTest { + val onlineExpected = listOf(AssignmentGroup(id = 1), AssignmentGroup(id = 2)) + val offlineExpected = listOf(AssignmentGroup(id = 3), AssignmentGroup(id = 4)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns offlineExpected + + val result = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + coVerify { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get assignment groups with assignments for grading period if device is offline`() = runTest { + val onlineExpected = listOf(AssignmentGroup(id = 1), AssignmentGroup(id = 2)) + val offlineExpected = listOf(AssignmentGroup(id = 3), AssignmentGroup(id = 4)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any(), any(), any()) } returns offlineExpected + + val result = repository.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + coVerify { localDataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get submissions for multiple assignments if device is online`() = runTest { + val onlineExpected = listOf(Submission(1)) + val offlineExpected = listOf(Submission(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getSubmissionsForMultipleAssignments(any(), any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getSubmissionsForMultipleAssignments(any(), any(), any(), any()) } returns offlineExpected + + val result = repository.getSubmissionsForMultipleAssignments(1, 1, listOf(1), true) + + coVerify { networkDataSource.getSubmissionsForMultipleAssignments(1, 1, listOf(1), true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get submissions for multiple assignments if device is offline`() = runTest { + val onlineExpected = listOf(Submission(1)) + val offlineExpected = listOf(Submission(2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getSubmissionsForMultipleAssignments(any(), any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getSubmissionsForMultipleAssignments(any(), any(), any(), any()) } returns offlineExpected + + val result = repository.getSubmissionsForMultipleAssignments(1, 1, listOf(1), true) + + coVerify { localDataSource.getSubmissionsForMultipleAssignments(1, 1, listOf(1), true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get course with syllabus if device is online`() = runTest { + val onlineExpected = listOf(Course(1)) + val offlineExpected = listOf(Course(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCoursesWithSyllabus(any()) } returns onlineExpected + coEvery { localDataSource.getCoursesWithSyllabus(any()) } returns offlineExpected + + val result = repository.getCoursesWithSyllabus(true) + + coVerify { networkDataSource.getCoursesWithSyllabus(true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get course with syllabus if device is offline`() = runTest { + val onlineExpected = listOf(Course(1)) + val offlineExpected = listOf(Course(2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getCoursesWithSyllabus(any()) } returns onlineExpected + coEvery { localDataSource.getCoursesWithSyllabus(any()) } returns offlineExpected + + val result = repository.getCoursesWithSyllabus(true) + + coVerify { localDataSource.getCoursesWithSyllabus(true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get grading periods for course if device is online`() = runTest { + val onlineExpected = listOf(GradingPeriod(1)) + val offlineExpected = listOf(GradingPeriod(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getGradingPeriodsForCourse(any(), any()) } returns onlineExpected + coEvery { localDataSource.getGradingPeriodsForCourse(any(), any()) } returns offlineExpected + + val result = repository.getGradingPeriodsForCourse(1, true) + + coVerify { networkDataSource.getGradingPeriodsForCourse(1, true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get grading periods for course if device is offline`() = runTest { + val onlineExpected = listOf(GradingPeriod(1)) + val offlineExpected = listOf(GradingPeriod(2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getGradingPeriodsForCourse(any(), any()) } returns onlineExpected + coEvery { localDataSource.getGradingPeriodsForCourse(any(), any()) } returns offlineExpected + + val result = repository.getGradingPeriodsForCourse(1, true) + + coVerify { localDataSource.getGradingPeriodsForCourse(1, true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get user enrollments for grading period if device is online`() = runTest { + val onlineExpected = listOf(Enrollment(1)) + val offlineExpected = listOf(Enrollment(2)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getUserEnrollmentsForGradingPeriod(any(), any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getUserEnrollmentsForGradingPeriod(any(), any(), any(), any()) } returns offlineExpected + + val result = repository.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) + + coVerify { networkDataSource.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get user enrollments for grading period if device is offline`() = runTest { + val onlineExpected = listOf(Enrollment(1)) + val offlineExpected = listOf(Enrollment(2)) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getUserEnrollmentsForGradingPeriod(any(), any(), any(), any()) } returns onlineExpected + coEvery { localDataSource.getUserEnrollmentsForGradingPeriod(any(), any(), any(), any()) } returns offlineExpected + + val result = repository.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) + + coVerify { localDataSource.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Get assignment groups with assignments if device is online`() = runTest { + val onlineExpected = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(2)))) + val offlineExpected = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(4)))) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns onlineExpected + coEvery { localDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns offlineExpected + + val result = repository.getAssignmentGroupsWithAssignments(1, true) + + coVerify { networkDataSource.getAssignmentGroupsWithAssignments(1, true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get assignment groups with assignments if device is offline`() = runTest { + val onlineExpected = listOf(AssignmentGroup(id = 1, assignments = listOf(Assignment(2)))) + val offlineExpected = listOf(AssignmentGroup(id = 3, assignments = listOf(Assignment(4)))) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns onlineExpected + coEvery { localDataSource.getAssignmentGroupsWithAssignments(any(), any()) } returns offlineExpected + + val result = repository.getAssignmentGroupsWithAssignments(1, true) + + coVerify { localDataSource.getAssignmentGroupsWithAssignments(1, true) } + assertEquals(offlineExpected, result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt new file mode 100644 index 0000000000..d29afbc786 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListLocalDataSourceTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades.datasource + +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class GradesListLocalDataSourceTest { + + private val courseFacade: CourseFacade = mockk(relaxed = true) + private val enrollmentFacade: EnrollmentFacade = mockk(relaxed = true) + private val assignmentFacade: AssignmentFacade = mockk(relaxed = true) + private val submissionFacade: SubmissionFacade = mockk(relaxed = true) + + private val dataSource = GradesListLocalDataSource(courseFacade, enrollmentFacade, assignmentFacade, submissionFacade) + + @Test + fun `Get course with grade successfully returns api model`() = runTest { + val expected = Course(1L) + + coEvery { courseFacade.getCourseById(any()) } returns expected + + val result = dataSource.getCourseWithGrade(1, true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get course with grade failure throws exception`() = runTest { + coEvery { courseFacade.getCourseById(any()) } returns null + + dataSource.getCourseWithGrade(1, true) + } + + @Test + fun `Get observee enrollments successfully returns api model`() = runTest { + val expected = listOf(Enrollment(1), Enrollment(2)) + + coEvery { enrollmentFacade.getAllEnrollments() } returns expected + + val result = dataSource.getObserveeEnrollments(true) + + assertEquals(expected, result) + } + + @Test + fun `Get assignment groups with assignments for grading period successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1L), AssignmentGroup(2L)) + + coEvery { assignmentFacade.getAssignmentGroupsWithAssignmentsForGradingPeriod(any(), any()) } returns expected + + val result = dataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + assertEquals(expected, result) + } + + @Test + fun `Get submissions by assignment ids successfully returns api model`() = runTest { + val expected = listOf(Submission(1), Submission(2)) + + coEvery { submissionFacade.findByAssignmentIds(listOf(1, 2)) } returns expected + + val result = dataSource.getSubmissionsForMultipleAssignments(1, 1, listOf(1, 2), true) + + assertEquals(expected, result) + } + + @Test + fun `Get courses with syllabus successfully returns api model`() = runTest { + val expected = listOf(Course(1), Course(2)) + + coEvery { courseFacade.getAllCourses() } returns expected + + val result = dataSource.getCoursesWithSyllabus(true) + + assertEquals(expected, result) + } + + @Test + fun `Get grading periods for course successfully returns api model`() = runTest { + val expected = listOf(GradingPeriod(1), GradingPeriod(2)) + + coEvery { courseFacade.getGradingPeriodsByCourseId(1) } returns expected + + val result = dataSource.getGradingPeriodsForCourse(1, true) + + assertEquals(expected, result) + } + + @Test + fun `Get user enrollments for grading period successfully returns api model`() = runTest { + val expected = listOf(Enrollment(1), Enrollment(2)) + + coEvery { enrollmentFacade.getEnrollmentsByGradingPeriodId(1) } returns expected + + val result = dataSource.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) + + assertEquals(expected, result) + } + + @Test + fun `Get assignment groups with assignments by course id successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1), AssignmentGroup(2)) + + coEvery { assignmentFacade.getAssignmentGroupsWithAssignments(1) } returns expected + + val result = dataSource.getAssignmentGroupsWithAssignments(1, true) + + assertEquals(expected, result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..b26fe9d0e9 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/grades/datasource/GradesListNetworkDataSourceTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.grades.datasource + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class GradesListNetworkDataSourceTest { + + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val submissionApi: SubmissionAPI.SubmissionInterface = mockk(relaxed = true) + + private val dataSource = GradesListNetworkDataSource(courseApi, enrollmentApi, assignmentApi, submissionApi) + + @Test + fun `Get course with grade successfully returns data`() = runTest { + val expected = Course(1) + + coEvery { courseApi.getCourseWithGrade(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getCourseWithGrade(1, true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get course with grade failure throws exception`() = runTest { + coEvery { courseApi.getCourseWithGrade(any(), any()) } returns DataResult.Fail() + + dataSource.getCourseWithGrade(1, true) + } + + @Test + fun `Get observee enrollments successfully returns data`() = runTest { + val expected = listOf(Enrollment(1)) + + coEvery { enrollmentApi.firstPageObserveeEnrollments(any()) } returns DataResult.Success(expected) + + val result = dataSource.getObserveeEnrollments(true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get observee enrollments failure throws exception`() = runTest { + coEvery { enrollmentApi.firstPageObserveeEnrollments(any()) } returns DataResult.Fail() + + dataSource.getObserveeEnrollments(true) + } + + @Test + fun `Get assignment groups with assignments for grading period successfully returns api model`() = runTest { + val expected = listOf(AssignmentGroup(1L), AssignmentGroup(2L)) + + coEvery { + assignmentApi.getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = dataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get assignment groups with assignments for grading period failure throws exception`() = runTest { + coEvery { + assignmentApi.getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + dataSource.getAssignmentGroupsWithAssignmentsForGradingPeriod(1, 1, scopeToStudent = true, forceNetwork = true) + } + + @Test + fun `Get submissions for multiple assignments successfully returns data`() = runTest { + val expected = listOf(Submission(1)) + + coEvery { submissionApi.getSubmissionsForMultipleAssignments(any(), any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getSubmissionsForMultipleAssignments(1, 1, listOf(1), true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get submissions for multiple assignments failure throws exception`() = runTest { + coEvery { submissionApi.getSubmissionsForMultipleAssignments(any(), any(), any(), any()) } returns DataResult.Fail() + + dataSource.getSubmissionsForMultipleAssignments(1, 1, listOf(1), true) + } + + @Test + fun `Get courses with syllabus successfully returns data`() = runTest { + val expected = listOf(Course(1)) + + coEvery { courseApi.firstPageCoursesWithSyllabus(any()) } returns DataResult.Success(expected) + + val result = dataSource.getCoursesWithSyllabus(true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get courses with syllabus failure throws exception`() = runTest { + coEvery { courseApi.firstPageCoursesWithSyllabus(any()) } returns DataResult.Fail() + + dataSource.getCoursesWithSyllabus(true) + } + + @Test + fun `Get grading period for course successfully returns data`() = runTest { + val expected = listOf(GradingPeriod(1)) + + coEvery { courseApi.getGradingPeriodsForCourse(any(), any()) } returns DataResult.Success(GradingPeriodResponse(expected)) + + val result = dataSource.getGradingPeriodsForCourse(1, true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get grading period for course failure throws exception`() = runTest { + coEvery { courseApi.getGradingPeriodsForCourse(any(), any()) } returns DataResult.Fail() + + dataSource.getGradingPeriodsForCourse(1, true) + } + + @Test + fun `Get user enrollments for grading period successfully returns data`() = runTest { + val expected = listOf(Enrollment(1)) + + coEvery { courseApi.getUserEnrollmentsForGradingPeriod(any(), any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get user enrollments for grading period failure throws exception`() = runTest { + coEvery { courseApi.getUserEnrollmentsForGradingPeriod(any(), any(), any(), any()) } returns DataResult.Fail() + + dataSource.getUserEnrollmentsForGradingPeriod(1, 1, 1, true) + } + + @Test + fun `Get assignment groups with assignments successfully returns data`() = runTest { + val expected = listOf(AssignmentGroup(1)) + + coEvery { assignmentApi.getFirstPageAssignmentGroupListWithAssignments(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getAssignmentGroupsWithAssignments(1, true) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get assignment groups with assignments failure throws exception`() = runTest { + coEvery { assignmentApi.getFirstPageAssignmentGroupListWithAssignments(any(), any()) } returns DataResult.Fail() + + dataSource.getAssignmentGroupsWithAssignments(1, true) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt new file mode 100644 index 0000000000..73bbcd4055 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/ModuleListRepositoryTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.modules.list.datasource.ModuleListLocalDataSource +import com.instructure.student.features.modules.list.datasource.ModuleListNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ModuleListRepositoryTest { + + private val networkDataSource: ModuleListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: ModuleListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = ModuleListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get all modules for course from network data source when device is online`() = runTest { + val offlineModules = listOf(ModuleObject(id = 1, name = "Offline"), ModuleObject(id = 2, name = "Offline 2")) + val onlineModules = listOf(ModuleObject(id = 3, name = "Online"), ModuleObject(id = 4, name = "Online 2")) + coEvery { networkDataSource.getAllModuleObjects(any(), any()) } returns DataResult.Success(onlineModules) + coEvery { localDataSource.getAllModuleObjects(any(), any()) } returns DataResult.Success(offlineModules) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getAllModuleObjects(Course(1), true) + + Assert.assertEquals(onlineModules, (result as DataResult.Success).data) + } + + @Test + fun `Get all modules for course from local data source when device is offline`() = runTest { + val offlineModules = listOf(ModuleObject(id = 1, name = "Offline"), ModuleObject(id = 2, name = "Offline 2")) + val onlineModules = listOf(ModuleObject(id = 3, name = "Online"), ModuleObject(id = 4, name = "Online 2")) + coEvery { networkDataSource.getAllModuleObjects(any(), any()) } returns DataResult.Success(onlineModules) + coEvery { localDataSource.getAllModuleObjects(any(), any()) } returns DataResult.Success(offlineModules) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getAllModuleObjects(Course(1), true) + + Assert.assertEquals(offlineModules, (result as DataResult.Success).data) + } + + @Test + fun `Get first page modules for course from network data source when device is online`() = runTest { + val offlineModules = listOf(ModuleObject(id = 1, name = "Offline"), ModuleObject(id = 2, name = "Offline 2")) + val onlineModules = listOf(ModuleObject(id = 3, name = "Online"), ModuleObject(id = 4, name = "Online 2")) + coEvery { networkDataSource.getFirstPageModuleObjects(any(), any()) } returns DataResult.Success(onlineModules) + coEvery { localDataSource.getFirstPageModuleObjects(any(), any()) } returns DataResult.Success(offlineModules) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getFirstPageModuleObjects(Course(1), true) + + Assert.assertEquals(onlineModules, (result as DataResult.Success).data) + } + + @Test + fun `Get first page modules for course from local data source when device is offline`() = runTest { + val offlineModules = listOf(ModuleObject(id = 1, name = "Offline"), ModuleObject(id = 2, name = "Offline 2")) + val onlineModules = listOf(ModuleObject(id = 3, name = "Online"), ModuleObject(id = 4, name = "Online 2")) + coEvery { networkDataSource.getFirstPageModuleObjects(any(), any()) } returns DataResult.Success(onlineModules) + coEvery { localDataSource.getFirstPageModuleObjects(any(), any()) } returns DataResult.Success(offlineModules) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getFirstPageModuleObjects(Course(1), true) + + Assert.assertEquals(offlineModules, (result as DataResult.Success).data) + } + + @Test + fun `Get next page modules for course from network data source`() = runTest { + val onlineModules = listOf(ModuleObject(id = 3, name = "Online"), ModuleObject(id = 4, name = "Online 2")) + coEvery { networkDataSource.getNextPageModuleObjects(any(), any()) } returns DataResult.Success(onlineModules) + + val result = repository.getNextPageModuleObjects("", true) + + Assert.assertEquals(onlineModules, (result as DataResult.Success).data) + } + + @Test + fun `Get first page module items for module from network data source when device is online`() = runTest { + val offlineItems = listOf(ModuleItem(id = 1, title = "Offline"), ModuleItem(id = 2, title = "Offline 2")) + val onlineItems = listOf(ModuleItem(id = 3, title = "Online"), ModuleItem(id = 4, title = "Online 2")) + coEvery { networkDataSource.getFirstPageModuleItems(any(), any(), any()) } returns DataResult.Success(onlineItems) + coEvery { localDataSource.getFirstPageModuleItems(any(), any(), any()) } returns DataResult.Success(offlineItems) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getFirstPageModuleItems(Course(1), 2, true) + + Assert.assertEquals(onlineItems, (result as DataResult.Success).data) + } + + @Test + fun `Get first page module items for module from local data source when device is offline`() = runTest { + val offlineItems = listOf(ModuleItem(id = 1, title = "Offline"), ModuleItem(id = 2, title = "Offline 2")) + val onlineItems = listOf(ModuleItem(id = 3, title = "Online"), ModuleItem(id = 4, title = "Online 2")) + coEvery { networkDataSource.getFirstPageModuleItems(any(), any(), any()) } returns DataResult.Success(onlineItems) + coEvery { localDataSource.getFirstPageModuleItems(any(), any(), any()) } returns DataResult.Success(offlineItems) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getFirstPageModuleItems(Course(1), 2, true) + + Assert.assertEquals(offlineItems, (result as DataResult.Success).data) + } + + @Test + fun `Get next page module items for module from network data source`() = runTest { + val onlineItems = listOf(ModuleItem(id = 3, title = "Online"), ModuleItem(id = 4, title = "Online 2")) + coEvery { networkDataSource.getNextPageModuleItems(any(), any()) } returns DataResult.Success(onlineItems) + + val result = repository.getNextPageModuleItems("", true) + + Assert.assertEquals(onlineItems, (result as DataResult.Success).data) + } + + @Test + fun `Get tabs from network data source when device is online`() = runTest { + val offlineTabs = listOf(Tab(tabId = "grades"), Tab(tabId = "modules")) + val onlineTabs = listOf(Tab(tabId = "grades online"), Tab(tabId = "modules online")) + coEvery { networkDataSource.getTabs(any(), any()) } returns DataResult.Success(onlineTabs) + coEvery { localDataSource.getTabs(any(), any()) } returns DataResult.Success(offlineTabs) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getTabs(Course(1), true) + + Assert.assertEquals(onlineTabs, result) + } + + @Test + fun `Get tabs from local data source when device is offline`() = runTest { + val offlineTabs = listOf(Tab(tabId = "grades"), Tab(tabId = "modules")) + val onlineTabs = listOf(Tab(tabId = "grades online"), Tab(tabId = "modules online")) + coEvery { networkDataSource.getTabs(any(), any()) } returns DataResult.Success(onlineTabs) + coEvery { localDataSource.getTabs(any(), any()) } returns DataResult.Success(offlineTabs) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getTabs(Course(1), true) + + Assert.assertEquals(offlineTabs, result) + } + + @Test + fun `Filter out hidden external tabs`() = runTest { + val onlineTabs = listOf( + Tab(tabId = "grades", isHidden = true), + Tab(tabId = "modules", type = Tab.TYPE_EXTERNAL), + Tab(tabId = "filtered out", type = Tab.TYPE_EXTERNAL, isHidden = true) + ) + coEvery { networkDataSource.getTabs(any(), any()) } returns DataResult.Success(onlineTabs) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getTabs(Course(1), true) + + Assert.assertEquals(2, result.size) + Assert.assertEquals(onlineTabs.first(), result.first()) + Assert.assertEquals(onlineTabs[1], result[1]) + } + + @Test + fun `Load curse settings from local storage when device is offline`() = runTest { + coEvery { networkDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = false) + coEvery { localDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = true) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.loadCourseSettings(1, true) + + Assert.assertTrue(result!!.restrictQuantitativeData) + } + + @Test + fun `Load curse settings from network when device is online`() = runTest { + coEvery { networkDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = false) + coEvery { localDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = true) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.loadCourseSettings(1, true) + + Assert.assertFalse(result!!.restrictQuantitativeData) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt new file mode 100644 index 0000000000..e27b27e40c --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListLocalDataSourceTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.ApiType +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.entities.TabEntity +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class ModuleListLocalDataSourceTest { + + private val tabDao = mockk(relaxed = true) + private val moduleFacade = mockk(relaxed = true) + private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + + private val dataSource = ModuleListLocalDataSource(tabDao, moduleFacade, courseSettingsDao) + + @Test + fun `getAllModuleObjects returns all module objects with DB api type`() = runTest { + val moduleObjects = listOf(ModuleObject(1, 1, "Module 1"), ModuleObject(2, 1, "Module 2")) + coEvery { moduleFacade.getModuleObjects(any()) } returns moduleObjects + + val result = dataSource.getAllModuleObjects(Course(1), true) + + Assert.assertEquals(ApiType.DB, (result as DataResult.Success).apiType) + Assert.assertEquals(moduleObjects, result.dataOrNull) + } + + @Test + fun `getFirstPageModuleObjects returns all module objects with DB api type`() = runTest { + val moduleObjects = listOf(ModuleObject(1, 1, "Module 1"), ModuleObject(2, 1, "Module 2")) + coEvery { moduleFacade.getModuleObjects(any()) } returns moduleObjects + + val result = dataSource.getFirstPageModuleObjects(Course(1), true) + + Assert.assertEquals(ApiType.DB, (result as DataResult.Success).apiType) + Assert.assertEquals(moduleObjects, result.dataOrNull) + } + + @Test + fun `getFirstPageModuleItems returns all module items with DB api type`() = runTest { + val moduleItems = listOf(ModuleItem(1, 1, 1, "Item 1"), ModuleItem(2, 1, 2, "Item 2")) + coEvery { moduleFacade.getModuleItems(any()) } returns moduleItems + + val result = dataSource.getFirstPageModuleItems(Course(1), 1, true) + + Assert.assertEquals(ApiType.DB, (result as DataResult.Success).apiType) + Assert.assertEquals(moduleItems, result.dataOrNull) + } + + @Test + fun `Convert tab entities to tab api models`() = runTest { + val tabEntities = listOf(TabEntity(Tab("modules"), 1), TabEntity(Tab("grades"), 1)) + coEvery { tabDao.findByCourseId(1) } returns tabEntities + + val result = dataSource.getTabs(Course(1), true) + + Assert.assertEquals(ApiType.DB, (result as DataResult.Success).apiType) + Assert.assertEquals(2, result.dataOrNull?.size) + Assert.assertEquals("modules", result.dataOrNull!![0].tabId) + Assert.assertEquals("grades", result.dataOrNull!![1].tabId) + } + + @Test + fun `Load course settings successfully returns api model`() = runTest { + val expected = CourseSettings(restrictQuantitativeData = true) + + coEvery { courseSettingsDao.findByCourseId(any()) } returns CourseSettingsEntity(expected, 1L) + + val result = dataSource.loadCourseSettings(1, true) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..72254ad8ad --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/modules/list/datasource/ModuleListNetworkDataSourceTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.list.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class ModuleListNetworkDataSourceTest { + + private val moduleApi: ModuleAPI.ModuleInterface = mockk(relaxed = true) + private val tabApi: TabAPI.TabsInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + + private val dataSource = ModuleListNetworkDataSource(moduleApi, tabApi, courseApi) + + @Test + fun `Return failed result when getAllModuleObjects fails`() = runTest { + coEvery { moduleApi.getFirstPageModuleObjects(any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.getAllModuleObjects(Course(1), true) + + Assert.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return successful result with all pages when from getAllModuleObjects`() = runTest { + val firstPageModules = listOf(ModuleObject(id = 1)) + val secondPageModules = listOf(ModuleObject(id = 2)) + coEvery { moduleApi.getFirstPageModuleObjects(any(), any(), any()) } returns DataResult.Success(firstPageModules, linkHeaders = LinkHeaders(nextUrl = "next")) + coEvery { moduleApi.getNextPageModuleObjectList(any(), any()) } returns DataResult.Success(secondPageModules) + + val result = dataSource.getAllModuleObjects(Course(1), true) + + Assert.assertEquals(2, (result as DataResult.Success).data.size) + Assert.assertEquals(firstPageModules.first(), result.data[0]) + Assert.assertEquals(secondPageModules.first(), result.data[1]) + } + + @Test + fun `Return failed result when getFirstPageModuleObjects fails`() = runTest { + coEvery { moduleApi.getFirstPageModuleObjects(any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.getFirstPageModuleObjects(Course(1), true) + + Assert.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return successful result from getFirstPageModuleObjects`() = runTest { + val firstPageModules = listOf(ModuleObject(id = 1)) + coEvery { moduleApi.getFirstPageModuleObjects(any(), any(), any()) } returns DataResult.Success(firstPageModules) + + val result = dataSource.getFirstPageModuleObjects(Course(1), true) + + Assert.assertEquals(1, (result as DataResult.Success).data.size) + Assert.assertEquals(firstPageModules.first(), result.data[0]) + } + + @Test + fun `Return failed result when getNextPageModuleObjects fails`() = runTest { + coEvery { moduleApi.getNextPageModuleObjectList(any(), any()) } returns DataResult.Fail() + + val result = dataSource.getNextPageModuleObjects("url", true) + + Assert.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return successful result with all pages when from getNextPageModuleObjects`() = runTest { + val secondPageModules = listOf(ModuleObject(id = 2)) + coEvery { moduleApi.getNextPageModuleObjectList(any(), any()) } returns DataResult.Success(secondPageModules) + + val result = dataSource.getNextPageModuleObjects("url", true) + + Assert.assertEquals(1, (result as DataResult.Success).data.size) + Assert.assertEquals(secondPageModules.first(), result.data[0]) + } + + @Test + fun `Return failed result when getFirstPageModuleItems fails`() = runTest { + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.getFirstPageModuleItems(Course(1), 1, true) + + Assert.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return successful result from getFirstPageModuleItems`() = runTest { + val firstPageModuleItems = listOf(ModuleItem(id = 1)) + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any()) } returns DataResult.Success(firstPageModuleItems) + + val result = dataSource.getFirstPageModuleItems(Course(1), 1, true) + + Assert.assertEquals(1, (result as DataResult.Success).data.size) + Assert.assertEquals(firstPageModuleItems.first(), result.data[0]) + } + + @Test + fun `Return failed result when getNextPageModuleItems fails`() = runTest { + coEvery { moduleApi.getNextPageModuleItemList(any(), any()) } returns DataResult.Fail() + + val result = dataSource.getNextPageModuleItems("url", true) + + Assert.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return successful result with all pages when from getNextPageModuleItems`() = runTest { + val secondPageModuleItems = listOf(ModuleItem(id = 2)) + coEvery { moduleApi.getNextPageModuleItemList(any(), any()) } returns DataResult.Success(secondPageModuleItems) + + val result = dataSource.getNextPageModuleItems("url", true) + + Assert.assertEquals(1, (result as DataResult.Success).data.size) + Assert.assertEquals(secondPageModuleItems.first(), result.data[0]) + } + + @Test + fun `Return failed result when getTabs fails`() = runTest { + coEvery { tabApi.getTabs(any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.getTabs(Course(1), true) + + Assert.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return successful result from getTabs`() = runTest { + val tabs = listOf(Tab(tabId = "modules"), Tab(tabId = "grades")) + coEvery { tabApi.getTabs(any(), any(), any()) } returns DataResult.Success(tabs) + + val result = dataSource.getTabs(Course(1), true) + + Assert.assertEquals(2, (result as DataResult.Success).data.size) + Assert.assertEquals(tabs.first(), result.data[0]) + Assert.assertEquals(tabs[1], result.data[1]) + } + + @Test + fun `Load course settings returns succesful api model`() = runTest { + val expected = CourseSettings(restrictQuantitativeData = true) + + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadCourseSettings(1, true) + + Assert.assertEquals(expected, result) + } + + @Test + fun `Load course settings failure returns null`() = runTest { + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Fail() + + val result = dataSource.loadCourseSettings(1, true) + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt new file mode 100644 index 0000000000..ef6ef2324d --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/modules/progression/ModuleProgressionRepositoryTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleItemWrapper +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionLocalDataSource +import com.instructure.student.features.modules.progression.datasource.ModuleProgressionNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.util.Date + +@ExperimentalCoroutinesApi +class ModuleProgressionRepositoryTest { + + private val localDataSource: ModuleProgressionLocalDataSource = mockk() + private val networkDataSource: ModuleProgressionNetworkDataSource = mockk() + private val networkStateProvider: NetworkStateProvider = mockk() + private val featureFlagProvider: FeatureFlagProvider = mockk() + private val courseSyncSettingsDao: CourseSyncSettingsDao = mockk() + private val localFileDao: LocalFileDao = mockk() + + private val repository = ModuleProgressionRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider, courseSyncSettingsDao, localFileDao) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get all module items from network when device is online`() = runTest { + val offlineItems = listOf(ModuleItem(id = 1, title = "Offline"), ModuleItem(id = 2, title = "Offline 2")) + val onlineItems = listOf(ModuleItem(id = 3, title = "Online"), ModuleItem(id = 4, title = "Online 2")) + coEvery { networkDataSource.getAllModuleItems(any(), any(), any()) } returns onlineItems + coEvery { localDataSource.getAllModuleItems(any(), any(), any()) } returns offlineItems + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getAllModuleItems(Course(1), 2, true) + + Assert.assertEquals(onlineItems, result) + } + + @Test + fun `Get all module items from local storage when device is offline`() = runTest { + val offlineItems = listOf(ModuleItem(id = 1, title = "Offline"), ModuleItem(id = 2, title = "Offline 2")) + val onlineItems = listOf(ModuleItem(id = 3, title = "Online"), ModuleItem(id = 4, title = "Online 2")) + coEvery { networkDataSource.getAllModuleItems(any(), any(), any()) } returns onlineItems + coEvery { localDataSource.getAllModuleItems(any(), any(), any()) } returns offlineItems + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getAllModuleItems(Course(1), 2, true) + + Assert.assertEquals(offlineItems, result) + } + + @Test + fun `Get module item sequence from network when device is online`() = runTest { + val offlineItems = ModuleItemSequence(items = arrayOf(ModuleItemWrapper(current = ModuleItem(id = 1, title = "offline")))) + val onlineItems = ModuleItemSequence(items = arrayOf(ModuleItemWrapper(current = ModuleItem(id = 2, title = "online")))) + coEvery { networkDataSource.getModuleItemSequence(any(), any(), any(), any()) } returns onlineItems + coEvery { localDataSource.getModuleItemSequence(any(), any(), any(), any()) } returns offlineItems + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getModuleItemSequence(Course(1), "Quiz", "1", true) + + Assert.assertEquals(onlineItems, result) + } + + @Test + fun `Get module item sequence from local storage when device is offline`() = runTest { + val offlineItems = ModuleItemSequence(items = arrayOf(ModuleItemWrapper(current = ModuleItem(id = 1, title = "offline")))) + val onlineItems = ModuleItemSequence(items = arrayOf(ModuleItemWrapper(current = ModuleItem(id = 2, title = "online")))) + coEvery { networkDataSource.getModuleItemSequence(any(), any(), any(), any()) } returns onlineItems + coEvery { localDataSource.getModuleItemSequence(any(), any(), any(), any()) } returns offlineItems + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getModuleItemSequence(Course(1), "Quiz", "1", true) + + Assert.assertEquals(offlineItems, result) + } + + @Test + fun `Get detailed quiz from network when device is online`() = runTest { + val offlineQuiz = Quiz(id = 1, title = "offline") + val onlineQuiz = Quiz(id = 2, title = "online") + coEvery { networkDataSource.getDetailedQuiz(any(), any(), any()) } returns onlineQuiz + coEvery { localDataSource.getDetailedQuiz(any(), any(), any()) } returns offlineQuiz + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.getDetailedQuiz("url", 1, true) + + Assert.assertEquals(onlineQuiz, result) + } + + @Test + fun `Get detailed quiz from local storage when device is offline`() = runTest { + val offlineQuiz = Quiz(id = 1, title = "offline") + val onlineQuiz = Quiz(id = 2, title = "online") + coEvery { networkDataSource.getDetailedQuiz(any(), any(), any()) } returns onlineQuiz + coEvery { localDataSource.getDetailedQuiz(any(), any(), any()) } returns offlineQuiz + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.getDetailedQuiz("url", 1, true) + + Assert.assertEquals(offlineQuiz, result) + } + + @Test + fun `Mark as not done calls network data source and returns it's successful result`() = runTest { + coEvery { networkDataSource.markAsNotDone(any(), any()) } returns DataResult.Success(mockk()) + + val result = repository.markAsNotDone(Course(1), ModuleItem(1)) + + coVerify { networkDataSource.markAsNotDone(Course(1), ModuleItem(1)) } + Assert.assertTrue(result.isSuccess) + } + + @Test + fun `Mark as not done calls network data source and returns it's failed result`() = runTest { + coEvery { networkDataSource.markAsNotDone(any(), any()) } returns DataResult.Fail() + + val result = repository.markAsNotDone(Course(1), ModuleItem(1)) + + coVerify { networkDataSource.markAsNotDone(Course(1), ModuleItem(1)) } + Assert.assertTrue(result.isFail) + } + + @Test + fun `Mark as done calls network data source and returns it's successful result`() = runTest { + coEvery { networkDataSource.markAsDone(any(), any()) } returns DataResult.Success(mockk()) + + val result = repository.markAsDone(Course(1), ModuleItem(1)) + + coVerify { networkDataSource.markAsDone(Course(1), ModuleItem(1)) } + Assert.assertTrue(result.isSuccess) + } + + @Test + fun `Mark done calls network data source and returns it's failed result`() = runTest { + coEvery { networkDataSource.markAsDone(any(), any()) } returns DataResult.Fail() + + val result = repository.markAsDone(Course(1), ModuleItem(1)) + + coVerify { networkDataSource.markAsDone(Course(1), ModuleItem(1)) } + Assert.assertTrue(result.isFail) + } + + @Test + fun `Mark as read calls network data source and returns it's successful result`() = runTest { + coEvery { networkDataSource.markAsRead(any(), any()) } returns DataResult.Success(mockk()) + + val result = repository.markAsRead(Course(1), ModuleItem(1)) + + coVerify { networkDataSource.markAsRead(Course(1), ModuleItem(1)) } + Assert.assertTrue(result.isSuccess) + } + + @Test + fun `Mark as read calls network data source and returns it's failed result`() = runTest { + coEvery { networkDataSource.markAsRead(any(), any()) } returns DataResult.Fail() + + val result = repository.markAsRead(Course(1), ModuleItem(1)) + + coVerify { networkDataSource.markAsRead(Course(1), ModuleItem(1)) } + Assert.assertTrue(result.isFail) + } + + @Test + fun `getSyncedTabs returns only the synced tabs from dao`() = runTest { + coEvery { courseSyncSettingsDao.findById(1) } returns CourseSyncSettingsEntity(1, "Course", false, mapOf( + "Page" to true, + "Quiz" to false, + "Assignment" to true, + "Files" to false + )) + + val result = repository.getSyncedTabs(1) + + Assert.assertEquals(setOf("Page", "Assignment"), result) + } + + @Test + fun `getSyncedFileIds returns only the synced file ids from dao`() = runTest { + coEvery { localFileDao.findByCourseId(1) } returns listOf( + LocalFileEntity(1, 1, Date(), "path"), LocalFileEntity(2, 1, Date(), "path2") + ) + + val result = repository.getSyncedFileIds(1) + + Assert.assertEquals(2, result.size) + Assert.assertTrue(result.contains(1)) + Assert.assertTrue(result.contains(2)) + } + + @Test + fun `getSyncedFileIds returns empty list if no file is found`() = runTest { + coEvery { localFileDao.findByCourseId(1) } returns emptyList() + + val result = repository.getSyncedFileIds(1) + + Assert.assertEquals(emptyList(), result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt new file mode 100644 index 0000000000..f9c090b82a --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionLocalDataSourceTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.QuizEntity +import com.instructure.pandautils.room.offline.facade.ModuleFacade +import com.instructure.student.features.modules.progression.ModuleItemAsset +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class ModuleProgressionLocalDataSourceTest { + + private val moduleFacade: ModuleFacade = mockk(relaxed = true) + private val quizDao: QuizDao = mockk() + + private val dataSource = ModuleProgressionLocalDataSource(moduleFacade, quizDao) + + @Test + fun `Get all module items from facade`() = runTest { + val moduleItems = listOf(ModuleItem(id = 1, title = "1"), ModuleItem(id = 2, title = "2")) + coEvery { moduleFacade.getModuleItems(any()) } returns moduleItems + + val result = dataSource.getAllModuleItems(Course(1), 1, true) + + coEvery { moduleFacade.getModuleItems(any()) } returns moduleItems + Assert.assertEquals(moduleItems, result) + } + + @Test + fun `Get quiz api model when entity is found in db`() = runTest { + val quiz = Quiz(id = 1, title = "Quiz 1") + coEvery { quizDao.findById(1) } returns QuizEntity(quiz, 1L) + + val result = dataSource.getDetailedQuiz("url", 1, true) + + Assert.assertEquals(quiz, result) + } + + @Test(expected = IllegalStateException::class) + fun `Throw exception when quiz is not found in db`() = runTest { + coEvery { quizDao.findById(1) } returns null + + dataSource.getDetailedQuiz("url", 1, true) + } + + @Test + fun `Return module item sequence for module item when asset type is module item`() = runTest { + val moduleItem = ModuleItem(id = 1, title = "1", type = "ModuleItem") + coEvery { moduleFacade.getModuleItemById(1) } returns moduleItem + coEvery { moduleFacade.getModuleObjectById(any()) } returns null + + val result = dataSource.getModuleItemSequence(Course(1), ModuleItemAsset.MODULE_ITEM.assetType, "1", true) + + coVerify { moduleFacade.getModuleItemById(1) } + Assert.assertEquals(1, result.items!!.size) + Assert.assertEquals(moduleItem, result.items?.first()?.current) + Assert.assertArrayEquals(emptyArray(), result.modules) + } + + @Test + fun `Return module item sequence for page when asset type is page`() = runTest { + val moduleItem = ModuleItem(id = 1, title = "1", type = "Page", pageUrl = "url") + coEvery { moduleFacade.getModuleItemForPage("url") } returns moduleItem + coEvery { moduleFacade.getModuleObjectById(any()) } returns null + + val result = dataSource.getModuleItemSequence(Course(1), ModuleItemAsset.PAGE.assetType, "url", true) + + coVerify { moduleFacade.getModuleItemForPage("url") } + Assert.assertEquals(1, result.items!!.size) + Assert.assertEquals(moduleItem, result.items?.first()?.current) + Assert.assertArrayEquals(emptyArray(), result.modules) + } + + @Test + fun `Return module item sequence for other asset type`() = runTest { + val moduleItem = ModuleItem(id = 1, title = "1", type = "Quiz") + coEvery { moduleFacade.getModuleItemByAssetIdAndType("Quiz", 2) } returns moduleItem + coEvery { moduleFacade.getModuleObjectById(any()) } returns null + + val result = dataSource.getModuleItemSequence(Course(1), ModuleItemAsset.QUIZ.assetType, "2", true) + + coVerify { moduleFacade.getModuleItemByAssetIdAndType("Quiz", 2) } + Assert.assertEquals(1, result.items!!.size) + Assert.assertEquals(moduleItem, result.items?.first()?.current) + Assert.assertArrayEquals(emptyArray(), result.modules) + } + + @Test + fun `Return module item sequence for other asset type with module object`() = runTest { + val moduleItem = ModuleItem(id = 1, title = "1", type = "Quiz", moduleId = 44) + val moduleObject = ModuleObject(id = 44, name = "Module 1", items = listOf(moduleItem)) + coEvery { moduleFacade.getModuleItemByAssetIdAndType("Quiz", 2) } returns moduleItem + coEvery { moduleFacade.getModuleObjectById(44) } returns moduleObject + + val result = dataSource.getModuleItemSequence(Course(1), ModuleItemAsset.QUIZ.assetType, "2", true) + + Assert.assertEquals(1, result.items!!.size) + Assert.assertEquals(moduleItem, result.items?.first()?.current) + Assert.assertEquals(1, result.modules!!.size) + Assert.assertEquals(moduleObject, result.modules!!.first()) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt new file mode 100644 index 0000000000..6f957f7fd1 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/modules/progression/datasource/ModuleProgressionNetworkDataSourceTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.modules.progression.datasource + +import com.instructure.canvasapi2.apis.ModuleAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleItemWrapper +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class ModuleProgressionNetworkDataSourceTest { + + private val moduleApi: ModuleAPI.ModuleInterface = mockk() + private val quizApi: QuizAPI.QuizInterface = mockk() + + private val dataSource = ModuleProgressionNetworkDataSource(moduleApi, quizApi) + + @Test(expected = IllegalStateException::class) + fun `Throw exception when getFirstPageModuleItems fails`() = runTest { + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any()) } returns DataResult.Fail() + + dataSource.getAllModuleItems(Course(1), 1, true) + } + + @Test + fun `Return successful result from getAllModuleItems`() = runTest { + val firstPageModuleItems = listOf(ModuleItem(id = 1)) + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any()) } returns DataResult.Success(firstPageModuleItems) + + val result = dataSource.getAllModuleItems(Course(1), 1, true) + + Assert.assertEquals(1, result.size) + Assert.assertEquals(firstPageModuleItems.first(), result[0]) + } + + @Test + fun `Return successful result from depaginated getAllModuleItems`() = runTest { + val firstPageModuleItems = listOf(ModuleItem(id = 1)) + val nextPageModuleItems = listOf(ModuleItem(id = 2)) + coEvery { moduleApi.getFirstPageModuleItems(any(), any(), any(), any()) } returns DataResult.Success(firstPageModuleItems, linkHeaders = LinkHeaders(nextUrl = "nextUrl")) + coEvery { moduleApi.getNextPageModuleItemList("nextUrl", any()) } returns DataResult.Success(nextPageModuleItems) + + val result = dataSource.getAllModuleItems(Course(1), 1, true) + + Assert.assertEquals(2, result.size) + Assert.assertEquals(firstPageModuleItems.first(), result[0]) + Assert.assertEquals(nextPageModuleItems.first(), result[1]) + } + + @Test(expected = IllegalStateException::class) + fun `Throw exception when getModuleItemSequence fails`() = runTest { + coEvery { moduleApi.getModuleItemSequence(any(), any(), any(), any(), any()) } returns DataResult.Fail() + + dataSource.getModuleItemSequence(Course(1), "Page", "1", true) + } + + @Test + fun `Return successful result from getModuleItemSequence`() = runTest { + val moduleItemSequence = ModuleItemSequence(items = arrayOf(ModuleItemWrapper(current = ModuleItem(id = 2)))) + coEvery { moduleApi.getModuleItemSequence(any(), any(), any(), any(), any()) } returns DataResult.Success(moduleItemSequence) + + val result = dataSource.getModuleItemSequence(Course(1), "Page", "1", true) + + Assert.assertEquals(1, result.items!!.size) + Assert.assertEquals(ModuleItem(2), result.items!!.first().current) + } + + @Test(expected = IllegalStateException::class) + fun `Throw exception when getDetailedQuiz fails`() = runTest { + coEvery { quizApi.getDetailedQuizByUrl(any(), any()) } returns DataResult.Fail() + + dataSource.getDetailedQuiz("url", 1, true) + } + + @Test + fun `Return successful result from getDetailedQuiz`() = runTest { + val quiz = Quiz(id = 1, title = "Quiz") + coEvery { quizApi.getDetailedQuizByUrl(any(), any()) } returns DataResult.Success(quiz) + + val result = dataSource.getDetailedQuiz("url", 1, true) + + Assert.assertEquals(1, result.id) + Assert.assertEquals("Quiz", result.title) + } + + @Test + fun `markAsNotDone returns data result from the api call`() = runTest { + val dataResult = DataResult.Success(mockk()) + coEvery { moduleApi.markModuleItemAsNotDone(any(), any(), any(), any(), any()) } returns dataResult + + val result = dataSource.markAsNotDone(Course(1), ModuleItem(id = 1, moduleId = 2)) + + coVerify { moduleApi.markModuleItemAsNotDone("courses", 1, 2, 1, any()) } + Assert.assertEquals(dataResult, result) + } + + @Test + fun `markAsDone returns data result from the api call`() = runTest { + val dataResult = DataResult.Success(mockk()) + coEvery { moduleApi.markModuleItemAsDone(any(), any(), any(), any(), any()) } returns dataResult + + val result = dataSource.markAsDone(Course(1), ModuleItem(id = 1, moduleId = 2)) + + coVerify { moduleApi.markModuleItemAsDone("courses", 1, 2, 1, any()) } + Assert.assertEquals(dataResult, result) + } + + @Test + fun `markAsRead returns data result from the api call`() = runTest { + val dataResult = DataResult.Success(mockk()) + coEvery { moduleApi.markModuleItemRead(any(), any(), any(), any(), any()) } returns dataResult + + val result = dataSource.markAsRead(Course(1), ModuleItem(id = 1, moduleId = 2)) + + coVerify { moduleApi.markModuleItemRead("courses", 1, 2, 1, any()) } + Assert.assertEquals(dataResult, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt new file mode 100644 index 0000000000..10163cc83a --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/navigation/NavigationRepositoryTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.navigation + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.navigation.datasource.NavigationLocalDataSource +import com.instructure.student.features.navigation.datasource.NavigationNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class NavigationRepositoryTest { + + private val networkDataSource: NavigationNetworkDataSource = mockk(relaxed = true) + private val localDataSource: NavigationLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = NavigationRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get course with grade if device is online`() = runTest { + val onlineExpected = Course(1) + val offlineExpected = Course(2) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCourse(any(), any()) } returns onlineExpected + coEvery { localDataSource.getCourse(any(), any()) } returns offlineExpected + + val result = repository.getCourse(1, true) + + coVerify { networkDataSource.getCourse(1, true) } + assertEquals(onlineExpected, result) + } + + @Test + fun `Get course if device is offline`() = runTest { + val onlineExpected = Course(1) + val offlineExpected = Course(2) + + every { networkStateProvider.isOnline() } returns false + coEvery { networkDataSource.getCourse(any(), any()) } returns onlineExpected + coEvery { localDataSource.getCourse(any(), any()) } returns offlineExpected + + val result = repository.getCourse(1, true) + + coVerify { localDataSource.getCourse(1, true) } + assertEquals(offlineExpected, result) + } + + @Test + fun `Is token valid returns true if network call succeeds`() = runTest { + coEvery { networkDataSource.getSelf() } returns DataResult.Success(User()) + + val result = repository.isTokenValid() + + assertTrue(result) + } + + @Test + fun `Is token valid returns false if network call fails`() = runTest { + coEvery { networkDataSource.getSelf() } returns DataResult.Fail() + + val result = repository.isTokenValid() + + assertFalse(result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt new file mode 100644 index 0000000000..dca80d96cc --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListLocalDataSourceTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.navigation.datasource + +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.facade.CourseFacade +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class NavigationLocalDataSourceTest { + + private val courseFacade: CourseFacade = mockk(relaxed = true) + + private val dataSource = NavigationLocalDataSource(courseFacade) + + @Test + fun `Get course successfully returns api model`() = runTest { + val expected = Course(1L) + + coEvery { courseFacade.getCourseById(any()) } returns expected + + val result = dataSource.getCourse(1, true) + + assertEquals(expected, result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..595d23c157 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/navigation/datasource/GradesListNetworkDataSourceTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.navigation.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +@ExperimentalCoroutinesApi +class NavigationNetworkDataSourceTest { + + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + + private val dataSource = NavigationNetworkDataSource(courseApi, userApi) + + @Test + fun `Get course successfully returns data`() = runTest { + val expected = Course(1) + + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.getCourse(1, true) + + assertEquals(expected, result) + } + @Test + fun `Get course failure returns null`() = runTest { + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Fail() + + val result = dataSource.getCourse(1, true) + + assertNull(result) + } + + @Test + fun `Get self returns success`() = runTest { + val expected = User(id = 55) + + coEvery { userApi.getSelf(any()) } returns DataResult.Success(expected) + + val result = dataSource.getSelf() + + assertEquals(expected, result.dataOrThrow) + } + + @Test + fun `Get self returns failure`() = runTest { + coEvery { userApi.getSelf(any()) } returns DataResult.Fail() + + val result = dataSource.getSelf() + + assertEquals(DataResult.Fail(), result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..de5834686f --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsLocalDataSourceTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.offline.assignmentdetails + +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.QuizEntity +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class AssignmentDetailsLocalDataSourceTest { + + private val courseFacade: CourseFacade = mockk(relaxed = true) + private val assignmentFacade: AssignmentFacade = mockk(relaxed = true) + private val quizDao: QuizDao = mockk(relaxed = true) + + private val dataSource = AssignmentDetailsLocalDataSource(courseFacade, assignmentFacade, quizDao) + + @Test + fun `Get course successfully returns api model`() = runTest { + val expected = Course(1) + coEvery { courseFacade.getCourseById(any()) } returns expected + + val course = dataSource.getCourseWithGrade(1, true) + + Assert.assertEquals(expected, course) + } + + @Test(expected = IllegalStateException::class) + fun `Get course failure throws exception`() = runTest { + coEvery { courseFacade.getCourseById(any()) } returns null + + dataSource.getCourseWithGrade(1, true) + } + + @Test + fun `Get assignment successfully returns api model`() = runTest { + val expected = Assignment(1) + coEvery { assignmentFacade.getAssignmentById(any()) } returns expected + + val assignment = dataSource.getAssignment(false, 1, 1, true) + + Assert.assertEquals(expected, assignment) + } + + @Test(expected = IllegalStateException::class) + fun `Get assignment failure throws exception`() = runTest { + coEvery { assignmentFacade.getAssignmentById(any()) } returns null + + dataSource.getAssignment(false, 1, 1, true) + } + + @Test + fun `Get quiz successfully returns api model`() = runTest { + val expected = Quiz(1) + coEvery { quizDao.findById(any()) } returns QuizEntity(expected, 1L) + + val quiz = dataSource.getQuiz(1, 1, true) + + Assert.assertEquals(expected, quiz) + } + + @Test(expected = IllegalStateException::class) + fun `Get quiz failure throws exception`() = runTest { + coEvery { quizDao.findById(any()) } returns null + + dataSource.getQuiz(1, 1, true) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..19c0fda613 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsNetworkDataSourceTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.offline.assignmentdetails + +import com.instructure.canvasapi2.apis.AssignmentAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.apis.SubmissionAPI +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.Failure +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class AssignmentDetailsNetworkDataSourceTest { + + private val coursesInterface: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val assignmentInterface: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val quizInterface: QuizAPI.QuizInterface = mockk(relaxed = true) + private val submissionInterface: SubmissionAPI.SubmissionInterface = mockk(relaxed = true) + + private val dataSource = AssignmentDetailsNetworkDataSource(coursesInterface, assignmentInterface, quizInterface, submissionInterface) + + @Test + fun `Get course successfully returns data`() = runTest { + val expected = Course(1) + coEvery { coursesInterface.getCourseWithGrade(any(), any()) } returns DataResult.Success(expected) + + val courseResult = dataSource.getCourseWithGrade(1, true) + + Assert.assertEquals(expected, courseResult) + } + + @Test(expected = IllegalStateException::class) + fun `Get course failure throws exception`() = runTest { + coEvery { coursesInterface.getCourseWithGrade(any(), any()) } returns DataResult.Fail() + + dataSource.getCourseWithGrade(1, true) + } + + @Test + fun `Get assignment as student successfully returns data`() = runTest { + val expected = Assignment(1) + coEvery { assignmentInterface.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Success(expected) + + val assignmentResult = dataSource.getAssignment(false, 1, 1, true) + + Assert.assertEquals(expected, assignmentResult) + } + + @Test + fun `Get assignment as observer successfully returns data`() = runTest { + val observeeAssignment = ObserveeAssignment(1, submissionList = listOf(Submission())) + val expected = observeeAssignment.toAssignmentForObservee() + coEvery { assignmentInterface.getAssignmentIncludeObservees(any(), any(), any()) } returns DataResult.Success(observeeAssignment) + + val assignmentResult = dataSource.getAssignment(true, 1, 1, true) + + Assert.assertEquals(expected, assignmentResult) + } + + @Test(expected = IllegalStateException::class) + fun `Get assignment failure throws exception`() = runTest { + coEvery { assignmentInterface.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Fail() + + dataSource.getAssignment(false, 1, 1, true) + } + + @Test(expected = IllegalAccessException::class) + fun `Get assignment failure throws IllegalAccessException on auth error`() = runTest { + coEvery { assignmentInterface.getAssignmentWithHistory(any(), any(), any()) } returns DataResult.Fail(failure = Failure.Authorization()) + + dataSource.getAssignment(false, 1, 1, true) + } + + @Test + fun `Get quiz successfully returns data`() = runTest { + val expected = Quiz() + coEvery { quizInterface.getQuiz(any(), any(), any()) } returns DataResult.Success(expected) + + val assignmentResult = dataSource.getQuiz(1, 1, true) + + Assert.assertEquals(expected, assignmentResult) + } + + @Test(expected = IllegalStateException::class) + fun `Get quiz failure throws exception`() = runTest { + coEvery { quizInterface.getQuiz(any(), any(), any()) } returns DataResult.Fail() + + dataSource.getQuiz(1, 1, true) + } + + @Test + fun `Get LTI by launch url successfully returns data`() = runTest { + val expected = LTITool() + coEvery { assignmentInterface.getExternalToolLaunchUrl(any(), any(), any(), any(), any()) } returns DataResult.Success(expected) + + val assignmentResult = dataSource.getExternalToolLaunchUrl(1, 1, 1, true) + + Assert.assertEquals(expected, assignmentResult) + } + + @Test(expected = IllegalStateException::class) + fun `Get LTI by launch url failure throws exception`() = runTest { + coEvery { assignmentInterface.getExternalToolLaunchUrl(any(), any(), any(), any(), any()) } returns DataResult.Fail() + + dataSource.getExternalToolLaunchUrl(1, 1, 1, true) + } + + @Test + fun `Get LTI by auth url successfully returns data`() = runTest { + val expected = LTITool() + coEvery { submissionInterface.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(expected) + + val assignmentResult = dataSource.getLtiFromAuthenticationUrl("", true) + + Assert.assertEquals(expected, assignmentResult) + } + + @Test(expected = IllegalStateException::class) + fun `Get LTI by auth url failure throws exception`() = runTest { + coEvery { submissionInterface.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Fail() + + dataSource.getLtiFromAuthenticationUrl("", true) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt new file mode 100644 index 0000000000..f569008123 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/offline/assignmentdetails/AssignmentDetailsRepositoryTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.offline.assignmentdetails + +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.LTITool +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.assignments.details.AssignmentDetailsRepository +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsLocalDataSource +import com.instructure.student.features.assignments.details.datasource.AssignmentDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class AssignmentDetailsRepositoryTest { + + private val networkDataSource: AssignmentDetailsNetworkDataSource = mockk(relaxed = true) + private val localDataSource: AssignmentDetailsLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = AssignmentDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get course if device is online`() = runTest { + val expected = Course(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getCourseWithGrade(any(), any()) } returns expected + + val course = repository.getCourseWithGrade(1, true) + + coVerify { networkDataSource.getCourseWithGrade(any(), any()) } + Assert.assertEquals(expected, course) + } + + @Test + fun `Get course if device is offline`() = runTest { + val expected = Course(1) + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getCourseWithGrade(any(), any()) } returns expected + + val course = repository.getCourseWithGrade(1, true) + + coVerify { localDataSource.getCourseWithGrade(any(), any()) } + Assert.assertEquals(expected, course) + } + + @Test + fun `Get assignment as observer if device is online`() = runTest { + val expected = Assignment(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAssignment(true, any(), any(), any()) } returns expected + + val assignment = repository.getAssignment(true, 1, 1, true) + + coVerify { networkDataSource.getAssignment(true, any(), any(), any()) } + Assert.assertEquals(expected, assignment) + } + + @Test + fun `Get assignment as student if device is online`() = runTest { + val expected = Assignment(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getAssignment(false, any(), any(), any()) } returns expected + + val assignment = repository.getAssignment(false, 1, 1, true) + + coVerify { networkDataSource.getAssignment(false, any(), any(), any()) } + Assert.assertEquals(expected, assignment) + } + + @Test + fun `Get assignment if device is offline`() = runTest { + val expected = Assignment(1) + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getAssignment(any(), any(), any(), any()) } returns expected + + val assignment = repository.getAssignment(false, 1, 1, true) + + coVerify { localDataSource.getAssignment(any(), any(), any(), any()) } + Assert.assertEquals(expected, assignment) + } + + @Test + fun `Get quiz if device is online`() = runTest { + val expected = Quiz(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getQuiz(any(), any(), any()) } returns expected + + val quiz = repository.getQuiz(1, 1, true) + + coVerify { networkDataSource.getQuiz(any(), any(), any()) } + Assert.assertEquals(expected, quiz) + } + + @Test + fun `Get quiz if device is offline`() = runTest { + val expected = Quiz(1) + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getQuiz(any(), any(), any()) } returns expected + + val assignment = repository.getQuiz(1, 1, true) + + coVerify { localDataSource.getQuiz(any(), any(), any()) } + Assert.assertEquals(expected, assignment) + } + + @Test + fun `Get external tool by launch url if device is online`() = runTest { + val expected = LTITool(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getExternalToolLaunchUrl(any(), any(), any(), any()) } returns expected + + val ltiTool = repository.getExternalToolLaunchUrl(1, 1, 1, true) + + coVerify { networkDataSource.getExternalToolLaunchUrl(any(), any(), any(), any()) } + Assert.assertEquals(expected, ltiTool) + } + + @Test + fun `Get external tool by launch url if device is offline`() = runTest { + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getExternalToolLaunchUrl(any(), any(), any(), any()) } returns null + + val ltiTool = repository.getExternalToolLaunchUrl(1, 1, 1, true) + + Assert.assertEquals(null, ltiTool) + } + + @Test + fun `Get external tool by authentication url if device is online`() = runTest { + val expected = LTITool(1) + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.getLtiFromAuthenticationUrl(any(), any()) } returns expected + + val ltiTool = repository.getLtiFromAuthenticationUrl("", true) + + coVerify { networkDataSource.getLtiFromAuthenticationUrl(any(), any()) } + Assert.assertEquals(expected, ltiTool) + } + + @Test + fun `Get external tool by authentication url if device is offline`() = runTest { + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getLtiFromAuthenticationUrl(any(), any()) } returns null + + val ltiTool = repository.getLtiFromAuthenticationUrl("", true) + + Assert.assertEquals(null, ltiTool) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt new file mode 100644 index 0000000000..51022eaac0 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserLocalDataSourceTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.offline.coursebrowser + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.TabEntity +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class CourseBrowserLocalDataSourceTest { + + private val tabDao: TabDao = mockk(relaxed = true) + private val pageFacade: PageFacade = mockk(relaxed = true) + private val courseSyncSettingsDao: CourseSyncSettingsDao = mockk(relaxed = true) + private val fileSyncSettingsDao: FileSyncSettingsDao = mockk(relaxed = true) + + private val dataSource = CourseBrowserLocalDataSource(tabDao, pageFacade, courseSyncSettingsDao, fileSyncSettingsDao) + + @Test + fun `Get tabs successfully returns api model`() = runTest { + coEvery { tabDao.findByCourseId(any()) } returns listOf(TabEntity(Tab(label = "Tab", tabId = "123"), 1)) + coEvery { courseSyncSettingsDao.findById(any()) } returns CourseSyncSettingsEntity(1, "Course", false, tabs = mapOf("123" to true)) + + val tabs = dataSource.getTabs(CanvasContext.emptyCourseContext(1), false) + + Assert.assertEquals(listOf(Tab(label = "Tab", tabId = "123")), tabs) + } + + @Test + fun `Get tabs with correct enabled state`() = runTest { + coEvery { tabDao.findByCourseId(any()) } returns listOf( + TabEntity(Tab(label = "Tab 1", tabId = "1"), 1), + TabEntity(Tab(label = "Tab 2", tabId = "2"), 1), + TabEntity(Tab(label = "Tab 3", tabId = "3"), 1) + ) + coEvery { courseSyncSettingsDao.findById(any()) } returns CourseSyncSettingsEntity( + 1, "Course", false, tabs = mapOf("1" to true, "2" to false) + ) + + val tabs = dataSource.getTabs(CanvasContext.emptyCourseContext(1), false) + + Assert.assertEquals( + listOf( + Tab(label = "Tab 1", tabId = "1", enabled = true), + Tab(label = "Tab 2", tabId = "2", enabled = false), + Tab(label = "Tab 3", tabId = "3", enabled = false) + ), tabs + ) + } + + @Test + fun `Enable files tab if full file sync is on`() = runTest { + coEvery { tabDao.findByCourseId(any()) } returns listOf( + TabEntity(Tab(label = "Tab 1", tabId = "1"), 1), + TabEntity(Tab(label = "Files", tabId = Tab.FILES_ID), 1), + ) + coEvery { courseSyncSettingsDao.findById(any()) } returns CourseSyncSettingsEntity( + 1, "Course", false, tabs = mapOf("1" to true), fullFileSync = true + ) + + val tabs = dataSource.getTabs(CanvasContext.emptyCourseContext(1), false) + + Assert.assertEquals( + listOf( + Tab(label = "Tab 1", tabId = "1", enabled = true), + Tab(label = "Files", tabId = Tab.FILES_ID, enabled = true) + ), tabs + ) + } + + @Test + fun `Enable files tab if any file is synced`() = runTest { + coEvery { tabDao.findByCourseId(any()) } returns listOf( + TabEntity(Tab(label = "Tab 1", tabId = "1"), 1), + TabEntity(Tab(label = "Files", tabId = Tab.FILES_ID), 1), + ) + coEvery { courseSyncSettingsDao.findById(any()) } returns CourseSyncSettingsEntity( + 1, "Course", false, tabs = mapOf("1" to true) + ) + coEvery { fileSyncSettingsDao.findByCourseId(1) } returns listOf(FileSyncSettingsEntity(1, "", 1, "")) + + val tabs = dataSource.getTabs(CanvasContext.emptyCourseContext(1), false) + + Assert.assertEquals( + listOf( + Tab(label = "Tab 1", tabId = "1", enabled = true), + Tab(label = "Files", tabId = Tab.FILES_ID, enabled = true) + ), tabs + ) + } + + @Test + fun `Do not enable files tab if no file is synced`() = runTest { + coEvery { tabDao.findByCourseId(any()) } returns listOf( + TabEntity(Tab(label = "Tab 1", tabId = "1"), 1), + TabEntity(Tab(label = "Files", tabId = Tab.FILES_ID), 1), + ) + coEvery { courseSyncSettingsDao.findById(any()) } returns CourseSyncSettingsEntity( + 1, "Course", false, tabs = mapOf("1" to true) + ) + coEvery { fileSyncSettingsDao.findByCourseId(1) } returns emptyList() + + val tabs = dataSource.getTabs(CanvasContext.emptyCourseContext(1), false) + + Assert.assertEquals( + listOf( + Tab(label = "Tab 1", tabId = "1", enabled = true), + Tab(label = "Files", tabId = Tab.FILES_ID, enabled = false) + ), tabs + ) + } + + @Test + fun `Get front page successfully returns api model`() = runTest { + coEvery { pageFacade.getFrontPage(any()) } returns Page(id = 12, title = "Page title", frontPage = true) + + val frontPage = dataSource.getFrontPage(CanvasContext.emptyCourseContext(1), false) + + Assert.assertEquals(Page(id = 12, title = "Page title", frontPage = true), frontPage) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt new file mode 100644 index 0000000000..91423513ce --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserNetworkDataSourceTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.offline.coursebrowser + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.apis.TabAPI +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class CourseBrowserNetworkDataSourceTest { + + private val tabApi: TabAPI.TabsInterface = mockk(relaxed = true) + private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true) + + private val dataSource = CourseBrowserNetworkDataSource(tabApi, pageApi) + + @Test + fun `Get tabs successfully returns data`() = runTest { + coEvery { tabApi.getTabs(any(), any(), any()) } returns DataResult.Success(listOf(Tab(label = "Tab"))) + + val tabs = dataSource.getTabs(CanvasContext.emptyCourseContext(1), true) + + Assert.assertEquals(listOf(Tab(label = "Tab")), tabs) + } + + @Test(expected = IllegalStateException::class) + fun `Get tabs failure throws exception`() = runTest { + coEvery { tabApi.getTabs(any(), any(), any()) } returns DataResult.Fail() + + dataSource.getTabs(CanvasContext.emptyCourseContext(1), true) + } + + @Test + fun `Get front page successfully returns data`() = runTest { + coEvery { pageApi.getFrontPage(any(), any(), any()) } returns DataResult.Success(Page(title = "Front page")) + + val frontPage = dataSource.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + Assert.assertEquals(Page(title = "Front page"), frontPage) + } + + @Test + fun `Get front page failure returns null`() = runTest { + coEvery { pageApi.getFrontPage(any(), any(), any()) } returns DataResult.Fail() + + val frontPage = dataSource.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + Assert.assertNull(frontPage) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt new file mode 100644 index 0000000000..d735c85a5d --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/offline/coursebrowser/CourseBrowserRepositoryTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.offline.coursebrowser + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.coursebrowser.CourseBrowserRepository +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserLocalDataSource +import com.instructure.student.features.coursebrowser.datasource.CourseBrowserNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class CourseBrowserRepositoryTest { + + private val networkDataSource: CourseBrowserNetworkDataSource = mockk(relaxed = true) + private val localDataSource: CourseBrowserLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val courseBrowserRepository = CourseBrowserRepository(networkDataSource, localDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Before + fun setUp() { + coEvery { networkDataSource.getTabs(any(), any()) } returns listOf(Tab(label = "Online")) + coEvery { localDataSource.getTabs(any(), any()) } returns listOf(Tab(label = "Offline")) + coEvery { networkDataSource.getFrontPage(any(), any()) } returns Page(title = "Online front page") + coEvery { localDataSource.getFrontPage(any(), any()) } returns Page(title = "Offline front page") + } + + @Test + fun `Get tabs from network if device is online`() = runTest { + every { networkStateProvider.isOnline() } returns true + + val tabs = courseBrowserRepository.getTabs(CanvasContext.emptyCourseContext(1), true) + + coVerify { networkDataSource.getTabs(any(), any()) } + Assert.assertEquals(listOf(Tab(label = "Online")), tabs) + } + + @Test + fun `Get local tabs if device is offline`() = runTest { + every { networkStateProvider.isOnline() } returns false + + val tabs = courseBrowserRepository.getTabs(CanvasContext.emptyCourseContext(1), true) + + coVerify { localDataSource.getTabs(any(), any()) } + Assert.assertEquals(listOf(Tab(label = "Offline")), tabs) + } + + @Test + fun `Get tabs filters external and local tabs`() = runTest { + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getTabs(any(), any()) } returns listOf( + Tab(label = "Offline"), + Tab(label = "External Hidden tab", type = Tab.TYPE_EXTERNAL, isHidden = true), + ) + + val tabs = courseBrowserRepository.getTabs(CanvasContext.emptyCourseContext(1), true) + + Assert.assertEquals(listOf(Tab(label = "Offline")), tabs) + } + + @Test + fun `Get front page from network if device is online`() = runTest { + every { networkStateProvider.isOnline() } returns true + + val frontPage = courseBrowserRepository.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + coVerify { networkDataSource.getFrontPage(any(), any()) } + Assert.assertEquals(Page(title = "Online front page"), frontPage) + } + + @Test + fun `Get local front page if device is offline`() = runTest { + every { networkStateProvider.isOnline() } returns false + + val frontPage = courseBrowserRepository.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + coVerify { localDataSource.getFrontPage(any(), any()) } + Assert.assertEquals(Page(title = "Offline front page"), frontPage) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..0c0f7a5f73 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsLocalDataSourceTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.student.features.pages.details.datasource.PageDetailsLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PageDetailsLocalDataSourceTest { + + private val pageFacade: PageFacade = mockk(relaxed = true) + + private lateinit var localDataSource: PageDetailsLocalDataSource + + @Before + fun setup() { + localDataSource = PageDetailsLocalDataSource(pageFacade) + } + + @Test + fun `Return front page data result`() = runTest { + val expected = Page(id = 1, title = "Page") + + coEvery { pageFacade.getFrontPage(1) } returns expected + + val result = localDataSource.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + pageFacade.getFrontPage(1) + } + } + + @Test + fun `Return failed data result if front page not found`() = runTest { + coEvery { pageFacade.getFrontPage(1) } returns null + + val result = localDataSource.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Fail(), result) + coVerify(exactly = 1) { + pageFacade.getFrontPage(1) + } + } + + @Test + fun `Return page details data result`() = runTest { + val expected = Page(id = 1, title = "Page") + + coEvery { pageFacade.getPageDetails(1, "id") } returns expected + + val result = localDataSource.getPageDetails(CanvasContext.emptyCourseContext(1), "id", true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + pageFacade.getPageDetails(1, "id") + } + } + + @Test + fun `Return failed data result if page details not found`() = runTest { + coEvery { pageFacade.getPageDetails(1, "id") } returns null + + val result = localDataSource.getPageDetails(CanvasContext.emptyCourseContext(1), "id", true) + + TestCase.assertEquals(DataResult.Fail(), result) + coVerify(exactly = 1) { + pageFacade.getPageDetails(1, "id") + } + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..fe72d85e46 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsNetworkDataSourceTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.pages.details.datasource.PageDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PageDetailsNetworkDataSourceTest { + + private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true) + + private lateinit var networkDataSource: PageDetailsNetworkDataSource + + @Before + fun setup() { + networkDataSource = PageDetailsNetworkDataSource(pageApi) + } + + @Test + fun `Return front page data result`() = runTest { + val expected = DataResult.Success(Page(id = 1, title = "Page")) + + coEvery { pageApi.getFrontPage(any(), any(), any()) } returns expected + + val result = networkDataSource.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + pageApi.getFrontPage(CanvasContext.emptyCourseContext(1).apiContext(), 1, RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if front page call fails`() = runTest { + coEvery { pageApi.getFrontPage(any(), any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getFrontPage(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return page details data result`() = runTest { + val expected = DataResult.Success(Page(id = 1, title = "Page")) + + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns expected + + val result = networkDataSource.getPageDetails(CanvasContext.emptyCourseContext(1), "id", true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + pageApi.getDetailedPage(CanvasContext.emptyCourseContext(1).apiContext(), 1, "id", RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if page details call fails`() = runTest { + coEvery { pageApi.getDetailedPage(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getPageDetails(CanvasContext.emptyCourseContext(1), "id", true) + + TestCase.assertEquals(DataResult.Fail(), result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt new file mode 100644 index 0000000000..c185fddebf --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/pages/details/PageDetailsRepositoryTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.features.pages.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.pages.details.datasource.PageDetailsLocalDataSource +import com.instructure.student.features.pages.details.datasource.PageDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PageDetailsRepositoryTest { + + private val networkDataSource: PageDetailsNetworkDataSource = mockk(relaxed = true) + private val localDataSource: PageDetailsLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private lateinit var repository: PageDetailsRepository + + @Before + fun setUp() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + repository = PageDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + } + + @Test + fun `Get front page from network if online`() = runTest { + val offlineExpected = DataResult.Success(Page(id = 1L, title = "Offline")) + val onlineExpected = DataResult.Success(Page(id = 2L, title = "Online")) + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.getFrontPage(any(), any()) } returns offlineExpected + coEvery { networkDataSource.getFrontPage(any(), any()) } returns onlineExpected + + val frontPage = repository.getFrontPage(CanvasContext.defaultCanvasContext(), true) + + TestCase.assertEquals(onlineExpected, frontPage) + } + + @Test + fun `Return failed result for front page if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getFrontPage(any(), any()) } returns DataResult.Fail() + + val result = repository.getFrontPage(CanvasContext.defaultCanvasContext(), false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Get front page from db if offline`() = runTest { + val offlineExpected = DataResult.Success(Page(id = 1L, title = "Offline")) + val onlineExpected = DataResult.Success(Page(id = 2L, title = "Online")) + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getFrontPage(any(), any()) } returns offlineExpected + coEvery { networkDataSource.getFrontPage(any(), any()) } returns onlineExpected + + val frontPage = repository.getFrontPage(CanvasContext.defaultCanvasContext(), true) + + TestCase.assertEquals(offlineExpected, frontPage) + } + + @Test + fun `Get page details from network if online`() = runTest { + val offlineExpected = DataResult.Success(Page(id = 1L, title = "Offline")) + val onlineExpected = DataResult.Success(Page(id = 2L, title = "Online")) + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.getPageDetails(any(), any(), any()) } returns offlineExpected + coEvery { networkDataSource.getPageDetails(any(), any(), any()) } returns onlineExpected + + val pageDetails = repository.getPageDetails(CanvasContext.defaultCanvasContext(), "id", true) + + TestCase.assertEquals(onlineExpected, pageDetails) + } + + @Test + fun `Return failed result for page details if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getPageDetails(any(), any(), any()) } returns DataResult.Fail() + + val result = repository.getPageDetails(CanvasContext.defaultCanvasContext(), "id", false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Get page details from db if offline`() = runTest { + val offlineExpected = DataResult.Success(Page(id = 1L, title = "Offline")) + val onlineExpected = DataResult.Success(Page(id = 2L, title = "Online")) + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.getPageDetails(any(), any(), any()) } returns offlineExpected + coEvery { networkDataSource.getPageDetails(any(), any(), any()) } returns onlineExpected + + val pageDetails = repository.getPageDetails(CanvasContext.defaultCanvasContext(), "id", true) + + TestCase.assertEquals(offlineExpected, pageDetails) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt new file mode 100644 index 0000000000..1f8a8e8ac8 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListLocalDataSourceTest.kt @@ -0,0 +1,30 @@ +package com.instructure.student.features.pages.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.pandautils.room.offline.facade.PageFacade +import com.instructure.student.features.pages.list.datasource.PageListLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class PageListLocalDataSourceTest { + + private val pageFacade: PageFacade = mockk(relaxed = true) + + private val dataSource = PageListLocalDataSource(pageFacade) + + @Test + fun `Returns Page api models`() = runTest { + val expected = listOf(Page(id = 1L, title = "Page 1"), Page(id = 2, title = "Page 2")) + coEvery { pageFacade.findByCourseId(any()) } returns expected + + val result = dataSource.loadPages(CanvasContext.defaultCanvasContext(), false) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..ad59542df3 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListNetworkDataSourceTest.kt @@ -0,0 +1,46 @@ +package com.instructure.student.features.pages.list + +import com.instructure.canvasapi2.apis.PageAPI +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.pages.list.datasource.PageListNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class PageListNetworkDataSourceTest { + + private val pageApi: PageAPI.PagesInterface = mockk(relaxed = true) + + private val dataSource = PageListNetworkDataSource(pageApi) + + @Test + fun `Return pages successfully`() = runTest { + val expected = listOf( + Page( + id = 1L, + title = "Page 1" + ), Page( + id = 2L, + title = "Page 2" + ) + ) + coEvery { pageApi.getFirstPagePages(any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadPages(CanvasContext.defaultCanvasContext(), false) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Get pages error throws exception`() = runTest { + coEvery { pageApi.getFirstPagePages(any(), any(), any()) } returns DataResult.Fail() + + dataSource.loadPages(CanvasContext.defaultCanvasContext(), true) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt new file mode 100644 index 0000000000..7639a70848 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/pages/list/PageListRepositoryTest.kt @@ -0,0 +1,59 @@ +package com.instructure.student.features.pages.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Page +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.features.pages.list.datasource.PageListLocalDataSource +import com.instructure.student.features.pages.list.datasource.PageListNetworkDataSource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PageListRepositoryTest { + + private val networkDataSource: PageListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: PageListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val pageListRepository = PageListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get tabs from network if online`() = runTest { + val offlineExpected = listOf(Page(id = 1L, title = "Offline")) + val onlineExpected = listOf(Page(id = 1L, title = "Online")) + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadPages(any(), any()) } returns offlineExpected + coEvery { networkDataSource.loadPages(any(), any()) } returns onlineExpected + + val pages = pageListRepository.loadPages(CanvasContext.defaultCanvasContext(), false) + + assertEquals(onlineExpected, pages) + } + + @Test + fun `Get tabs from db if offline`() = runTest { + val offlineExpected = listOf(Page(id = 1L, title = "Offline")) + val onlineExpected = listOf(Page(id = 1L, title = "Online")) + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadPages(any(), any()) } returns offlineExpected + coEvery { networkDataSource.loadPages(any(), any()) } returns onlineExpected + + val pages = pageListRepository.loadPages(CanvasContext.defaultCanvasContext(), false) + + assertEquals(offlineExpected, pages) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt new file mode 100644 index 0000000000..3084926b6d --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/people/details/PeopleDetailsRepositoryTest.kt @@ -0,0 +1,86 @@ +package com.instructure.student.features.people.details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PeopleDetailsRepositoryTest { + private val networkDataSource: PeopleDetailsNetworkDataSource = mockk(relaxed = true) + private val localDataSource: PeopleDetailsLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = PeopleDetailsRepository(networkDataSource, localDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get user if device is online`() = runTest { + val offlineExpected = User(id = 1L, name = "Offline") + val onlineExpected = User(id = 1L, name = "Online") + + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadUser(any(), any(), any()) } returns offlineExpected + coEvery { networkDataSource.loadUser(any(), any(), any()) } returns onlineExpected + + val result = repository.loadUser(CanvasContext.defaultCanvasContext(), 1L, true) + + TestCase.assertEquals(onlineExpected, result) + } + + @Test + fun `Get user if device is offline`() = runTest { + val offlineExpected = User(id = 1L, name = "Offline") + val onlineExpected = User(id = 1L, name = "Online") + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadUser(any(), any(), any()) } returns offlineExpected + coEvery { networkDataSource.loadUser(any(), any(), any()) } returns onlineExpected + + val result = repository.loadUser(CanvasContext.defaultCanvasContext(), 1L, false) + + TestCase.assertEquals(offlineExpected, result) + } + + @Test + fun `Get permission if device is online`() = runTest { + val offlineExpected = false + val onlineExpected = true + + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadMessagePermission(any(), any(), any(), any()) } returns offlineExpected + coEvery { networkDataSource.loadMessagePermission(any(), any(), any(), any()) } returns onlineExpected + + val result = repository.loadMessagePermission(CanvasContext.defaultCanvasContext(), User(1L), true) + + TestCase.assertEquals(onlineExpected, result) + } + + @Test + fun `Get permission if device is offline`() = runTest { + val offlineExpected = true + val onlineExpected = false + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadMessagePermission(any(), any(), any(), any()) } returns offlineExpected + coEvery { networkDataSource.loadMessagePermission(any(), any(), any(), any()) } returns onlineExpected + + val result = repository.loadMessagePermission(CanvasContext.defaultCanvasContext(), User(1L), false) + + TestCase.assertEquals(offlineExpected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..bb886804ca --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsLocalDataSourceTest.kt @@ -0,0 +1,46 @@ +package com.instructure.student.features.people.details.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.facade.UserFacade +import com.instructure.student.features.people.details.PeopleDetailsLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class PeopleDetailsLocalDataSourceTest { + private val userFacade: UserFacade = mockk(relaxed = true) + private val dataSource = PeopleDetailsLocalDataSource(userFacade) + + @Test + fun `User is returned by id`() = runTest { + val expected = User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))) + coEvery { userFacade.getUserById(any()) } returns expected + + val result = dataSource.loadUser(CanvasContext.defaultCanvasContext(), 1L) + + TestCase.assertEquals(expected, result) + } + + @Test + fun `User is not exists`() = runTest { + val expected = null + coEvery { userFacade.getUserById(any()) } returns null + + val result = dataSource.loadUser(CanvasContext.defaultCanvasContext(), 2L) + + TestCase.assertEquals(expected, result) + } + + @Test + fun `Permission is always false`() = runTest { + val expected = false + val result = dataSource.loadMessagePermission(CanvasContext.defaultCanvasContext(), listOf("test"), User(), false) + TestCase.assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..788a5a0448 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/people/details/datasource/PeopleDetailsNetworkDataSourceTest.kt @@ -0,0 +1,68 @@ +package com.instructure.student.features.people.details.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.people.details.PeopleDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PeopleDetailsNetworkDataSourceTest { + private val userAPI: UserAPI.UsersInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true) + private val dataSource = PeopleDetailsNetworkDataSource(userAPI, courseApi, groupApi) + + @Test + fun `User Api returns data`() = runTest { + val expected = User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))) + + coEvery { userAPI.getUserForContextId(any(), any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadUser(CanvasContext.defaultCanvasContext(), 1L) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `User Api first page fail`() = runTest { + coEvery { userAPI.getUserForContextId(any(), any(), any(), any()) } returns DataResult.Fail() + + dataSource.loadUser(CanvasContext.defaultCanvasContext(), 1L) + + } + + @Test + fun `Permission api returns data if it is a course`() = runTest { + val expected: Boolean = true + val groupCanvasContext = Course() + coEvery { courseApi.getCoursePermissions(any(), any(), any()) } returns DataResult.Success(CanvasContextPermission(send_messages = expected)) + coEvery { groupApi.getGroupPermissions(any(), any(), any()) } returns DataResult.Success(CanvasContextPermission(send_messages = !expected)) + + val result = dataSource.loadMessagePermission(groupCanvasContext, listOf("test"), User(), false) + + assertEquals(expected, result) + } + + @Test + fun `Permission api returns data if it is a group`() = runTest { + val expected: Boolean = true + val groupCanvasContext = Group() + coEvery { groupApi.getGroupPermissions(any(), any(), any()) } returns DataResult.Success(CanvasContextPermission(send_messages = expected)) + coEvery { courseApi.getCoursePermissions(any(), any(), any()) } returns DataResult.Success(CanvasContextPermission(send_messages = !expected)) + + val result = dataSource.loadMessagePermission(groupCanvasContext, listOf("test"), User(), false) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt new file mode 100644 index 0000000000..194221b9c8 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/people/list/PeopleListRepositoryTest.kt @@ -0,0 +1,143 @@ +package com.instructure.student.features.people.list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class PeopleListRepositoryTest { + private val networkDataSource: PeopleListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: PeopleListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = PeopleListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get course people first page if device is online`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadFirstPagePeople(any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadFirstPagePeople(any(), any()) } returns DataResult.Success(onlineExpected) + + val people = repository.loadFirstPagePeople(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(onlineExpected, people) + } + + @Test + fun `Get course people first page if device is offline`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadFirstPagePeople(any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadFirstPagePeople(any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadFirstPagePeople(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(offlineExpected, pages) + } + + @Test + fun `Get course people next page if device is online`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadNextPagePeople(any(), any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadNextPagePeople(any(), any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadNextPagePeople(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(onlineExpected, pages) + } + + @Test + fun `Get course people next page if device is offline`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadNextPagePeople(any(), any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadNextPagePeople(any(), any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadNextPagePeople(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(offlineExpected, pages) + } + + @Test + fun `Get course teachers if device is online`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadTeachers(any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadTeachers(any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadTeachers(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(onlineExpected, pages) + } + + @Test + fun `Get course teachers if device is offline`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadTeachers(any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadTeachers(any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadTeachers(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(offlineExpected, pages) + } + + @Test + fun `Get course TAs if device is online`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns true + coEvery { localDataSource.loadTAs(any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadTAs(any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadTAs(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(onlineExpected, pages) + } + + @Test + fun `Get course TAs if device is offline`() = runTest { + val offlineExpected = listOf(User(id = 1L, name = "Offline")) + val onlineExpected = listOf(User(id = 2L, name = "Online")) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadTAs(any(), any()) } returns DataResult.Success(offlineExpected) + coEvery { networkDataSource.loadTAs(any(), any()) } returns DataResult.Success(onlineExpected) + + val pages = repository.loadTAs(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(offlineExpected, pages) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt new file mode 100644 index 0000000000..d06e2b5b09 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListLocalDataSourceTest.kt @@ -0,0 +1,71 @@ +package com.instructure.student.features.people.list.datasource + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.facade.UserFacade +import com.instructure.student.features.people.list.PeopleListLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class PeopleListLocalDataSourceTest { + private val userFacade: UserFacade = mockk(relaxed = true) + private val dataSource = PeopleListLocalDataSource(userFacade) + + @Test + fun `People list returned as first page`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + coEvery { userFacade.getUsersByCourseId(any()) } returns expected + + val people = dataSource.loadFirstPagePeople(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(expected, people) + } + + @Test + fun `Next page returns no data`() = runTest { + val expected = emptyList() + coEvery { userFacade.getUsersByCourseId(any()) } returns expected + + val people = dataSource.loadNextPagePeople(CanvasContext.defaultCanvasContext(), false, "nextUrl").dataOrNull + + assertEquals(expected, people) + } + + @Test + fun `Teacher list returned`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + coEvery { userFacade.getUsersByCourseIdAndRole(any(), any()) } returns expected + + val people = dataSource.loadTeachers(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(expected, people) + } + + @Test + fun `TA list returned`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + coEvery { userFacade.getUsersByCourseIdAndRole(any(), any()) } returns expected + + val people = dataSource.loadTeachers(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(expected, people) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..6cdaa8ecf5 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/people/list/datasource/PeopleListNetworkDataSourceTest.kt @@ -0,0 +1,117 @@ +package com.instructure.student.features.people.list.datasource + +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.people.list.PeopleListNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class PeopleListNetworkDataSourceTest { + private val userAPI: UserAPI.UsersInterface = mockk(relaxed = true) + private val dataSource = PeopleListNetworkDataSource(userAPI) + + @Test + fun `User Api first page successfully returns data`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + + coEvery { userAPI.getFirstPagePeopleList(any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadFirstPagePeople(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(expected, result) + } + + @Test + fun `User Api first page fail`() = runTest { + coEvery { userAPI.getFirstPagePeopleList(any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.loadFirstPagePeople(CanvasContext.defaultCanvasContext(), true) + assertEquals(DataResult.Fail(), result) + + } + + @Test + fun `User Api next page successfully returns data`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + + coEvery { userAPI.getNextPagePeopleList(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadNextPagePeople(CanvasContext.defaultCanvasContext(), false, "nextUrl").dataOrNull + + assertEquals(expected, result) + } + + @Test + fun `User Api next page fail`() = runTest { + coEvery { userAPI.getNextPagePeopleList(any(), any()) } returns DataResult.Fail() + + val result = dataSource.loadNextPagePeople(CanvasContext.defaultCanvasContext(), false, "nextUrl") + assertEquals(DataResult.Fail(), result) + + } + + @Test + fun `User Api successfully returns teachers`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + + coEvery { userAPI.getFirstPagePeopleList(any(), any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadTeachers(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(expected, result) + } + + @Test + fun `User Api teachers fail`() = runTest { + coEvery { userAPI.getFirstPagePeopleList(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.loadTeachers(CanvasContext.defaultCanvasContext(), true) + assertEquals(DataResult.Fail(), result) + + } + + @Test + fun `User Api successfully returns TAs`() = runTest { + val expected = listOf( + User(id = 1L, name = "User 1", enrollments = listOf(Enrollment(1L), Enrollment(2L))), + User(id = 2L, name = "User 2", enrollments = listOf(Enrollment(3L))), + User(id = 3L, name = "User 3", enrollments = listOf()) + ) + + coEvery { userAPI.getFirstPagePeopleList(any(), any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadTAs(CanvasContext.defaultCanvasContext(), false).dataOrNull + + assertEquals(expected, result) + } + + @Test + fun `User Api TAs fail`() = runTest { + coEvery { userAPI.getFirstPagePeopleList(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = dataSource.loadTAs(CanvasContext.defaultCanvasContext(), true) + assertEquals(DataResult.Fail(), result) + + } + +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt new file mode 100644 index 0000000000..82535495db --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/quiz/list/QuizListRepositoryTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.quiz.list + +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class QuizListRepositoryTest { + + private val networkDataSource: QuizListNetworkDataSource = mockk(relaxed = true) + private val localDataSource: QuizListLocalDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private val repository = QuizListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + + @Before + fun setup() = runTest { + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Get course quizzes first page if device is online`() = runTest { + val expected = listOf(Quiz(id = 1L)) + + every { networkStateProvider.isOnline() } returns true + coEvery { networkDataSource.loadQuizzes(any(), any(), any()) } returns expected + + val result = repository.loadQuizzes("courses", 1L, false) + + assertEquals(expected, result) + } + + @Test + fun `Get course quizzes first page if device is offline`() = runTest { + val expected = listOf(Quiz(id = 1L)) + + every { networkStateProvider.isOnline() } returns false + coEvery { localDataSource.loadQuizzes(any(), any(), any()) } returns expected + + val result = repository.loadQuizzes("courses", 1L, false) + + assertEquals(expected, result) + } + + @Test + fun `Load curse settings from local storage when device is offline`() = runTest { + coEvery { networkDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = false) + coEvery { localDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = true) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.loadCourseSettings(1, true) + + Assert.assertTrue(result!!.restrictQuantitativeData) + } + + @Test + fun `Load curse settings from network when device is online`() = runTest { + coEvery { networkDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = false) + coEvery { localDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = true) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.loadCourseSettings(1, true) + + Assert.assertFalse(result!!.restrictQuantitativeData) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt new file mode 100644 index 0000000000..c737dd80dc --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListLocalDataSourceTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.quiz.list.datasource + +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.entities.QuizEntity +import com.instructure.student.features.quiz.list.QuizListLocalDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class QuizListLocalDataSourceTest { + private val quizDao: QuizDao = mockk(relaxed = true) + private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + + private val dataSource = QuizListLocalDataSource(quizDao, courseSettingsDao) + + @Test + fun `Quizzes returned`() = runTest { + val expected = listOf(Quiz(1L), Quiz(2L)) + + coEvery { quizDao.findByCourseId(any()) } returns expected.map { QuizEntity(it, 1L) } + + val result = dataSource.loadQuizzes("course", 1L, false) + assertEquals(expected, result) + } + + @Test + fun `Load course settings successfully returns api model`() = runTest { + val expected = CourseSettings(restrictQuantitativeData = true) + + coEvery { courseSettingsDao.findByCourseId(any()) } returns CourseSettingsEntity(expected, 1L) + + val result = dataSource.loadCourseSettings(1, true) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..ece2def15f --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/features/quiz/list/datasource/QuizListNetworkDataSourceTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.features.quiz.list.datasource + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.QuizAPI +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.features.quiz.list.QuizListNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class QuizListNetworkDataSourceTest { + private val quizApi: QuizAPI.QuizInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + + private val dataSource = QuizListNetworkDataSource(quizApi, courseApi) + + @Test + fun `Quizzes are returned`() = runTest { + val expected = listOf(Quiz(1L), Quiz(2L)) + + coEvery { quizApi.getFirstPageQuizzesList(any(), any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadQuizzes("contextType", 1L, false) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `Quizzes Api fail`() = runTest { + coEvery { quizApi.getFirstPageQuizzesList(any(), any(), any()) } returns DataResult.Fail() + + dataSource.loadQuizzes("contextType", 1L, false) + } + + @Test + fun `Load course settings returns succesful api model`() = runTest { + val expected = CourseSettings(restrictQuantitativeData = true) + + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Success(expected) + + val result = dataSource.loadCourseSettings(1, true) + + Assert.assertEquals(expected, result) + } + + @Test + fun `Load course settings failure returns null`() = runTest { + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Fail() + + val result = dataSource.loadCourseSettings(1, true) + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/adapter/DiscussionListRecyclerAdapterTest.kt b/apps/student/src/test/java/com/instructure/student/test/adapter/DiscussionListRecyclerAdapterTest.kt index e89ae466a4..4127cd5ee2 100644 --- a/apps/student/src/test/java/com/instructure/student/test/adapter/DiscussionListRecyclerAdapterTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/adapter/DiscussionListRecyclerAdapterTest.kt @@ -21,7 +21,9 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.student.adapter.DiscussionListRecyclerAdapter +import com.instructure.student.features.discussion.list.DiscussionListRepository +import com.instructure.student.features.discussion.list.adapter.DiscussionListRecyclerAdapter +import io.mockk.mockk import junit.framework.TestCase import org.junit.Before import org.junit.Test @@ -36,10 +38,8 @@ class DiscussionListRecyclerAdapterTest : TestCase() { private lateinit var topicHeader: DiscussionTopicHeader private lateinit var topicHeader2: DiscussionTopicHeader - - class DiscussionListRecyclerAdapterWrapper(context: Context) : DiscussionListRecyclerAdapter(context, CanvasContext.emptyCourseContext(), true, object : DiscussionListRecyclerAdapter.AdapterToDiscussionsCallback { - override fun discussionOverflow(group: String?, discussionTopicHeader: DiscussionTopicHeader) {} - override fun askToDeleteDiscussion(discussionTopicHeader: DiscussionTopicHeader) {} + class DiscussionListRecyclerAdapterWrapper(context: Context) : DiscussionListRecyclerAdapter(context, CanvasContext.emptyCourseContext(), true, mockk(relaxed = true), mockk(relaxed = true), + object : DiscussionListRecyclerAdapter.AdapterToDiscussionsCallback { override fun onRefreshStarted() {} override fun onRowClicked(discussionTopicHeader: DiscussionTopicHeader, position: Int, isOpenDetail: Boolean) {} override fun onRefreshFinished() {} diff --git a/apps/student/src/test/java/com/instructure/student/test/adapter/FileListRecyclerAdapterTest.kt b/apps/student/src/test/java/com/instructure/student/test/adapter/FileListRecyclerAdapterTest.kt index 72a1ee1e9b..5612dc4d23 100644 --- a/apps/student/src/test/java/com/instructure/student/test/adapter/FileListRecyclerAdapterTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/adapter/FileListRecyclerAdapterTest.kt @@ -22,8 +22,11 @@ import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.FileFolder -import com.instructure.student.adapter.FileFolderCallback -import com.instructure.student.adapter.FileListRecyclerAdapter +import com.instructure.student.features.files.list.FileFolderCallback +import com.instructure.student.features.files.list.FileListRecyclerAdapter +import com.instructure.student.features.files.list.FileListRepository +import io.mockk.mockk +import io.mockk.mockkClass import junit.framework.TestCase import org.junit.Before import org.junit.Test @@ -37,11 +40,12 @@ class FileListRecyclerAdapterTest : TestCase() { private lateinit var fileFolder: FileFolder private lateinit var fileFolder2: FileFolder - class FileListRecyclerAdapterWrapper(context: Context) : FileListRecyclerAdapter(context, CanvasContext.emptyCourseContext(), emptyList(), FileFolder(), object : FileFolderCallback { + class FileListRecyclerAdapterWrapper(context: Context) : FileListRecyclerAdapter(context, CanvasContext.emptyCourseContext(), emptyList(), FileFolder(), object : + FileFolderCallback { override fun onItemClicked(item: FileFolder) {} override fun onOpenItemMenu(item: FileFolder, anchorView: View) {} override fun onRefreshFinished() {} - }, true) + }, fileListRepository = mockk(relaxed = true)) @Before fun setup() { diff --git a/apps/student/src/test/java/com/instructure/student/test/adapter/GradesListRecyclerAdapterTest.kt b/apps/student/src/test/java/com/instructure/student/test/adapter/GradesListRecyclerAdapterTest.kt index 48bd9e3a84..38070c203b 100644 --- a/apps/student/src/test/java/com/instructure/student/test/adapter/GradesListRecyclerAdapterTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/adapter/GradesListRecyclerAdapterTest.kt @@ -22,7 +22,9 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Submission -import com.instructure.student.adapter.GradesListRecyclerAdapter +import com.instructure.student.features.grades.GradesListRecyclerAdapter +import com.instructure.student.features.grades.GradesListRepository +import io.mockk.mockk import junit.framework.TestCase import org.junit.Assert import org.junit.Before @@ -31,22 +33,31 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class GradesListRecyclerAdapterTest : TestCase() { - private var mAdapter: GradesListRecyclerAdapter? = null + private var adapter: GradesListRecyclerAdapter? = null + + private val repository: GradesListRepository = mockk(relaxed = true) /** * Make it so the protected constructor can be called */ - class GradesListRecyclerAdapterWrapper(context: Context) : GradesListRecyclerAdapter(context) + class GradesListRecyclerAdapterWrapper( + context: Context, + repository: GradesListRepository + ) : GradesListRecyclerAdapter( + context, + onGradingPeriodResponse = {}, + repository = repository + ) @Before fun setup() { - mAdapter = GradesListRecyclerAdapterWrapper(ApplicationProvider.getApplicationContext()) + adapter = GradesListRecyclerAdapterWrapper(ApplicationProvider.getApplicationContext(), repository) } @Test fun testAreContentsTheSame_SameNameAndPoints() { val assignment = Assignment(name = "assignment", pointsPossible = 0.0) - Assert.assertTrue(mAdapter!!.createItemCallback().areContentsTheSame(assignment, assignment)) + Assert.assertTrue(adapter!!.createItemCallback().areContentsTheSame(assignment, assignment)) } @Test @@ -54,7 +65,7 @@ class GradesListRecyclerAdapterTest : TestCase() { val assignment1 = Assignment(name = "assignment1", pointsPossible = 0.0) val assignment2 = Assignment(name = "assignment2", pointsPossible = 0.0) - Assert.assertFalse(mAdapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) + Assert.assertFalse(adapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) } @Test @@ -62,7 +73,7 @@ class GradesListRecyclerAdapterTest : TestCase() { val assignment1 = Assignment(name = "assignment1", pointsPossible = 0.0) val assignment2 = Assignment(name = "assignment1", pointsPossible = 1.0) - Assert.assertFalse(mAdapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) + Assert.assertFalse(adapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) } @Test @@ -70,7 +81,7 @@ class GradesListRecyclerAdapterTest : TestCase() { val submission = Submission(grade = "A") val assignment = Assignment(name = "assignment", pointsPossible = 0.0, submission = submission) - Assert.assertTrue(mAdapter!!.createItemCallback().areContentsTheSame(assignment, assignment)) + Assert.assertTrue(adapter!!.createItemCallback().areContentsTheSame(assignment, assignment)) } @Test @@ -79,7 +90,7 @@ class GradesListRecyclerAdapterTest : TestCase() { val assignment1 = Assignment(name = "assignment", pointsPossible = 0.0, submission = submission1) val assignment2 = Assignment(name = "assignment1", pointsPossible = 0.0, submission = null) - Assert.assertFalse(mAdapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) + Assert.assertFalse(adapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) } @Test @@ -90,6 +101,6 @@ class GradesListRecyclerAdapterTest : TestCase() { val submission2 = Submission(grade = null) val assignment2 = Assignment(name = "assignment1", pointsPossible = 0.0, submission = submission2) - Assert.assertFalse(mAdapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) + Assert.assertFalse(adapter!!.createItemCallback().areContentsTheSame(assignment1, assignment2)) } } diff --git a/apps/student/src/test/java/com/instructure/student/test/adapter/ModuleListRecyclerAdapterTest.kt b/apps/student/src/test/java/com/instructure/student/test/adapter/ModuleListRecyclerAdapterTest.kt index 8029b5e5d5..9c614661f1 100644 --- a/apps/student/src/test/java/com/instructure/student/test/adapter/ModuleListRecyclerAdapterTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/adapter/ModuleListRecyclerAdapterTest.kt @@ -22,7 +22,8 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.utils.ContextKeeper -import com.instructure.student.adapter.ModuleListRecyclerAdapter +import com.instructure.student.features.modules.list.adapter.ModuleListRecyclerAdapter +import io.mockk.mockk import junit.framework.TestCase import org.junit.Before import org.junit.Test @@ -37,7 +38,7 @@ class ModuleListRecyclerAdapterTest : TestCase() { /** * Make it so the protected constructor can be called */ - class ModuleListRecyclerAdapterWrapper(context: Context) : ModuleListRecyclerAdapter(context) + class ModuleListRecyclerAdapterWrapper(context: Context) : ModuleListRecyclerAdapter(context, mockk(), mockk()) @Before fun setup() { diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt index 93ea7990e7..25667582e4 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsEffectHandlerTest.kt @@ -15,15 +15,14 @@ */ package com.instructure.student.test.assignment.details.submissionDetails -import com.instructure.canvasapi2.managers.* +import com.instructure.canvasapi2.managers.CourseManager +import com.instructure.canvasapi2.managers.ExternalToolManager +import com.instructure.canvasapi2.managers.FeaturesManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.Failure -import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsContentType -import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEffect -import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEffectHandler -import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsEvent +import com.instructure.student.mobius.assignmentDetails.submissionDetails.* import com.instructure.student.mobius.assignmentDetails.submissionDetails.drawer.comments.SubmissionCommentsSharedEvent import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsView import com.instructure.student.mobius.common.ChannelSource @@ -42,7 +41,8 @@ import java.util.concurrent.Executors class SubmissionDetailsEffectHandlerTest : Assert() { private val view: SubmissionDetailsView = mockk(relaxed = true) - private val effectHandler = SubmissionDetailsEffectHandler().apply { view = this@SubmissionDetailsEffectHandlerTest.view } + private val repository: SubmissionDetailsRepository = mockk(relaxed = true) + private val effectHandler = SubmissionDetailsEffectHandler(repository).apply { view = this@SubmissionDetailsEffectHandlerTest.view } private val eventConsumer: Consumer = mockk(relaxed = true) private val connection = effectHandler.connect(eventConsumer) @@ -67,21 +67,13 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val assignmentId = 1L val errorMessage = "Error" - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) - mockkObject(EnrollmentManager) + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Fail(Failure.Network(errorMessage)) - every { EnrollmentManager.getObserveeEnrollmentsAsync(any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(Failure.Network(errorMessage)) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Fail(Failure.Network(errorMessage)) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(Failure.Network(errorMessage)) - } + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Fail(Failure.Network(errorMessage)) - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(Failure.Network(errorMessage)) - } + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Fail(Failure.Network(errorMessage)) mockkObject(ApiPrefs) every { ApiPrefs.user } returns user @@ -97,7 +89,7 @@ class SubmissionDetailsEffectHandlerTest : Assert() { isStudioEnabled = false, quizResult = null, studioLTIToolResult = DataResult.Fail(null), - assignmentEnhancementsEnabled = true + assignmentEnhancementsEnabled = false ) ) } @@ -112,16 +104,13 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val assignmentId = 1L val errorMessage = "Error" - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Fail(Failure.Authorization(errorMessage)) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(Failure.Authorization(errorMessage)) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Fail(Failure.Authorization(errorMessage)) - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(Failure.Authorization(errorMessage)) - } + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Fail(Failure.Authorization(errorMessage)) + + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Fail(Failure.Authorization(errorMessage)) mockkObject(ApiPrefs) every { ApiPrefs.user } returns user @@ -137,7 +126,7 @@ class SubmissionDetailsEffectHandlerTest : Assert() { isStudioEnabled = false, quizResult = null, studioLTIToolResult = DataResult.Fail(null), - assignmentEnhancementsEnabled = true + assignmentEnhancementsEnabled = false ) ) } @@ -163,25 +152,15 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val user = User() val ltiTool = LTITool(url = "https://www.instructure.com") - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) - mockkObject(EnrollmentManager) + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Success(listOf()) - every { EnrollmentManager.getObserveeEnrollmentsAsync(any()) } returns mockk { - coEvery { await() } returns DataResult.Success(listOf()) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Success(assignment) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignment) - } + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Success(submission) - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(submission) - } + coEvery { repository.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(ltiTool) - every { SubmissionManager.getLtiFromAuthenticationUrlAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(ltiTool) - } + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Success(listOf("assignments_2_student")) mockkObject(ApiPrefs) every { ApiPrefs.user } returns user @@ -222,25 +201,15 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val submission = Submission() val user = User() - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) - mockkObject(EnrollmentManager) + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Success(listOf()) - every { EnrollmentManager.getObserveeEnrollmentsAsync(any()) } returns mockk { - coEvery { await() } returns DataResult.Success(listOf()) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Success(assignment) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignment) - } + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Success(submission) - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(submission) - } + coEvery { repository.getExternalToolLaunchUrl(any(), any(), any(), any()) } returns DataResult.Success(ltiTool) - every { AssignmentManager.getExternalToolLaunchUrlAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(ltiTool) - } + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Success(listOf("assignments_2_student")) mockkObject(ApiPrefs) every { ApiPrefs.user } returns user @@ -274,25 +243,15 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val user = User() val ltiTool = LTITool(url = "https://www.instructure.com") - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) - mockkObject(EnrollmentManager) + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Success(listOf(observeeEnrollment)) - every { EnrollmentManager.getObserveeEnrollmentsAsync(any()) } returns mockk { - coEvery { await() } returns DataResult.Success(listOf(observeeEnrollment)) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Success(assignment) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignment) - } + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Success(submission) - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), observerId, any()) } returns mockk { - coEvery { await() } returns DataResult.Success(submission) - } + coEvery { repository.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Success(ltiTool) - every { SubmissionManager.getLtiFromAuthenticationUrlAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(ltiTool) - } + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Success(listOf("assignments_2_student")) mockkObject(ApiPrefs) every { ApiPrefs.user } returns user @@ -316,8 +275,6 @@ class SubmissionDetailsEffectHandlerTest : Assert() { confirmVerified(eventConsumer) } - - @Test fun `loadData gets quiz if assignment is a quiz`() { val courseId = 1L @@ -327,25 +284,17 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val quiz = Quiz(quizId) val user = User() - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) - mockkObject(QuizManager) + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Success(listOf()) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignment) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Success(assignment) - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(submission) - } + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Success(submission) - every { SubmissionManager.getLtiFromAuthenticationUrlAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(null) - } + coEvery { repository.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Fail(null) - every { QuizManager.getQuizAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(quiz) - } + coEvery { repository.getQuiz(any(), any(), any()) } returns DataResult.Success(quiz) + + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Success(listOf("assignments_2_student")) mockkObject(ApiPrefs) every { ApiPrefs.user } returns user @@ -378,21 +327,19 @@ class SubmissionDetailsEffectHandlerTest : Assert() { val user = User() val studioLTITool = LTITool(url = "instructuremedia.com/lti/launch") - mockkObject(AssignmentManager) - mockkObject(SubmissionManager) mockkObject(ExternalToolManager) - every { AssignmentManager.getAssignmentAsync(any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignment) - } + coEvery { repository.isOnline() } returns true - every { SubmissionManager.getSingleSubmissionAsync(any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(submission) - } + coEvery { repository.getObserveeEnrollments(any()) } returns DataResult.Success(listOf()) - every { SubmissionManager.getLtiFromAuthenticationUrlAsync(any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail(null) - } + coEvery { repository.getAssignment(any(), any(), any()) } returns DataResult.Success(assignment) + + coEvery { repository.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Success(submission) + + coEvery { repository.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Fail(null) + + coEvery { repository.getCourseFeatures(any(), any()) } returns DataResult.Success(listOf()) every { ExternalToolManager.getExternalToolsForCanvasContextAsync(any(), any()) } returns mockk { coEvery { await() } returns DataResult.Success(listOf(studioLTITool)) @@ -412,7 +359,7 @@ class SubmissionDetailsEffectHandlerTest : Assert() { isStudioEnabled = true, quizResult = null, studioLTIToolResult = DataResult.Success(studioLTITool), - assignmentEnhancementsEnabled = true + assignmentEnhancementsEnabled = false ) ) } @@ -424,10 +371,12 @@ class SubmissionDetailsEffectHandlerTest : Assert() { fun `ShowSubmissionContentType results in view calling showSubmissionContent`() { val contentType = SubmissionDetailsContentType.NoneContent + coEvery { repository.isOnline() } returns true + connection.accept(SubmissionDetailsEffect.ShowSubmissionContentType(contentType)) verify(timeout = 100) { - view.showSubmissionContent(contentType) + view.showSubmissionContent(contentType, true) } confirmVerified(view) diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..95c41a623a --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsLocalDataSourceTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.assignment.details.submissionDetails + +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.entities.QuizEntity +import com.instructure.pandautils.room.offline.facade.AssignmentFacade +import com.instructure.pandautils.room.offline.facade.EnrollmentFacade +import com.instructure.pandautils.room.offline.facade.SubmissionFacade +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SubmissionDetailsLocalDataSourceTest { + + private val enrollmentFacade: EnrollmentFacade = mockk(relaxed = true) + private val submissionFacade: SubmissionFacade = mockk(relaxed = true) + private val assignmentFacade: AssignmentFacade = mockk(relaxed = true) + private val quizDao: QuizDao = mockk(relaxed = true) + private val courseFeaturesDao: CourseFeaturesDao = mockk(relaxed = true) + private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + private lateinit var localDataSource: SubmissionDetailsLocalDataSource + + @Before + fun setup() { + localDataSource = + SubmissionDetailsLocalDataSource(enrollmentFacade, submissionFacade, assignmentFacade, quizDao, courseFeaturesDao, courseSettingsDao) + } + + @Test + fun `Return observee enrollment api model list`() = runTest { + val expected = listOf(Enrollment(1), Enrollment(2)) + + coEvery { enrollmentFacade.getAllEnrollments() } returns expected + + val result = localDataSource.getObserveeEnrollments(true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + enrollmentFacade.getAllEnrollments() + } + } + + @Test + fun `Return empty observee enrollment api model list if enrollments not found`() = runTest { + val expected = emptyList() + + coEvery { enrollmentFacade.getAllEnrollments() } returns expected + + val result = localDataSource.getObserveeEnrollments(true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + enrollmentFacade.getAllEnrollments() + } + } + + @Test + fun `Return submission api model`() = runTest { + val expected = Submission(1) + + coEvery { submissionFacade.findByAssignmentId(1) } returns expected + + val result = localDataSource.getSingleSubmission(1, 1, 1, true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + submissionFacade.findByAssignmentId(1) + } + } + + @Test + fun `Return failed data result if submission not found`() = runTest { + coEvery { submissionFacade.findByAssignmentId(1) } returns null + + val result = localDataSource.getSingleSubmission(1, 1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + coVerify(exactly = 1) { + submissionFacade.findByAssignmentId(1) + } + } + + @Test + fun `Return assignment api model`() = runTest { + val expected = Assignment(1) + + coEvery { assignmentFacade.getAssignmentById(1) } returns expected + + val result = localDataSource.getAssignment(1, 1, true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + assignmentFacade.getAssignmentById(1) + } + } + + @Test + fun `Return failed data result if assignment not found`() = runTest { + coEvery { assignmentFacade.getAssignmentById(1) } returns null + + val result = localDataSource.getAssignment(1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + coVerify(exactly = 1) { + assignmentFacade.getAssignmentById(1) + } + } + + @Test + fun `Return failed data result for external tool launch url call`() = runTest { + val result = localDataSource.getExternalToolLaunchUrl(1, 1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return failed data result for lti from authentication url call`() = runTest { + val result = localDataSource.getLtiFromAuthenticationUrl("url", true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return quiz api model`() = runTest { + val expected = Quiz(1) + + coEvery { quizDao.findById(1) } returns QuizEntity(expected, 1L) + + val result = localDataSource.getQuiz(1, 1, true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + quizDao.findById(1) + } + } + + @Test + fun `Return failed data result if quiz not found`() = runTest { + coEvery { quizDao.findById(1) } returns null + + val result = localDataSource.getQuiz(1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + coVerify(exactly = 1) { + quizDao.findById(1) + } + } + + @Test + fun `Return course features api model`() = runTest { + val expected = listOf("feature") + + coEvery { courseFeaturesDao.findByCourseId(1) } returns CourseFeaturesEntity(1, expected) + + val result = localDataSource.getCourseFeatures(1, true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + courseFeaturesDao.findByCourseId(1) + } + } + + @Test + fun `Return failed data result if course features not found`() = runTest { + coEvery { courseFeaturesDao.findByCourseId(1) } returns null + + val result = localDataSource.getCourseFeatures(1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + coVerify(exactly = 1) { + courseFeaturesDao.findByCourseId(1) + } + } + + @Test + fun `Load course settings successfully returns api model`() = runTest { + val expected = CourseSettings(restrictQuantitativeData = true) + + coEvery { courseSettingsDao.findByCourseId(any()) } returns CourseSettingsEntity(expected, 1L) + + val result = localDataSource.loadCourseSettings(1, true) + + assertEquals(expected, result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..1473a2a8c2 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsNetworkDataSourceTest.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.assignment.details.submissionDetails + +import com.instructure.canvasapi2.apis.* +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SubmissionDetailsNetworkDataSourceTest { + + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) + private val submissionApi: SubmissionAPI.SubmissionInterface = mockk(relaxed = true) + private val assignmentApi: AssignmentAPI.AssignmentInterface = mockk(relaxed = true) + private val quizApi: QuizAPI.QuizInterface = mockk(relaxed = true) + private val featuresApi: FeaturesAPI.FeaturesInterface = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private lateinit var networkDataSource: SubmissionDetailsNetworkDataSource + + @Before + fun setup() { + networkDataSource = SubmissionDetailsNetworkDataSource(enrollmentApi, submissionApi, assignmentApi, quizApi, featuresApi, courseApi) + } + + @Test + fun `Return observee enrollment api model list`() = runTest { + val expected = DataResult.Success(listOf(Enrollment(1), Enrollment(2))) + coEvery { enrollmentApi.firstPageObserveeEnrollments(any()) } returns expected + + val result = networkDataSource.getObserveeEnrollments(true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + enrollmentApi.firstPageObserveeEnrollments(RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if observee enrollments call fails`() = runTest { + coEvery { enrollmentApi.firstPageObserveeEnrollments(any()) } returns DataResult.Fail() + + val result = networkDataSource.getObserveeEnrollments(true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return submission api model`() = runTest { + val expected = DataResult.Success(Submission(1)) + coEvery { submissionApi.getSingleSubmission(any(), any(), any(), any()) } returns expected + + val result = networkDataSource.getSingleSubmission(1, 1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + submissionApi.getSingleSubmission(1, 1, 1, RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if submission call fails`() = runTest { + coEvery { submissionApi.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getSingleSubmission(1, 1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return assignment api model`() = runTest { + val expected = DataResult.Success(Assignment(1)) + coEvery { assignmentApi.getAssignment(any(), any(), any()) } returns expected + + val result = networkDataSource.getAssignment(1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + assignmentApi.getAssignment(1, 1, RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if assignment call fails`() = runTest { + coEvery { assignmentApi.getAssignment(any(), any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getAssignment(1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return external tool launch url api model`() = runTest { + val expected = DataResult.Success(LTITool(1)) + coEvery { assignmentApi.getExternalToolLaunchUrl(any(), any(), any(), any(), any()) } returns expected + + val result = networkDataSource.getExternalToolLaunchUrl(1, 1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + assignmentApi.getExternalToolLaunchUrl(1, 1, 1, restParams = RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if external tool launch url call fails`() = runTest { + coEvery { assignmentApi.getExternalToolLaunchUrl(any(), any(), any(), any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getExternalToolLaunchUrl(1, 1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return lti api model from authentication url`() = runTest { + val expected = DataResult.Success(LTITool(1)) + coEvery { submissionApi.getLtiFromAuthenticationUrl(any(), any()) } returns expected + + val result = networkDataSource.getLtiFromAuthenticationUrl("url", true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + submissionApi.getLtiFromAuthenticationUrl("url", RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if lti from authentication url call fails`() = runTest { + coEvery { submissionApi.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getLtiFromAuthenticationUrl("url", true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return quiz api model`() = runTest { + val expected = DataResult.Success(Quiz(1)) + coEvery { quizApi.getQuiz(any(), any(), any()) } returns expected + + val result = networkDataSource.getQuiz(1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + quizApi.getQuiz(1, 1, RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if quiz call fails`() = runTest { + coEvery { quizApi.getQuiz(any(), any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getQuiz(1, 1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return course features api model`() = runTest { + val expected = DataResult.Success(listOf("feature")) + coEvery { featuresApi.getEnabledFeaturesForCourse(any(), any()) } returns expected + + val result = networkDataSource.getCourseFeatures(1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + featuresApi.getEnabledFeaturesForCourse(1, RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed data result if course features call fails`() = runTest { + coEvery { featuresApi.getEnabledFeaturesForCourse(any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.getCourseFeatures(1, true) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Load course settings returns succesful api model`() = runTest { + val expected = CourseSettings(restrictQuantitativeData = true) + + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Success(expected) + + val result = networkDataSource.loadCourseSettings(1, true) + + Assert.assertEquals(expected, result) + } + + @Test + fun `Load course settings failure returns null`() = runTest { + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Fail() + + val result = networkDataSource.loadCourseSettings(1, true) + + Assert.assertNull(result) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt new file mode 100644 index 0000000000..af3a83944b --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submissionDetails/SubmissionDetailsRepositoryTest.kt @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.assignment.details.submissionDetails + +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.assignmentDetails.submissionDetails.SubmissionDetailsRepository +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsLocalDataSource +import com.instructure.student.mobius.assignmentDetails.submissionDetails.datasource.SubmissionDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SubmissionDetailsRepositoryTest { + + private val localDataSource: SubmissionDetailsLocalDataSource = mockk(relaxed = true) + private val networkDataSource: SubmissionDetailsNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private lateinit var repository: SubmissionDetailsRepository + + @Before + fun setUp() { + repository = SubmissionDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Return observee enrollments if online`() = runTest { + val expected = DataResult.Success(listOf(Enrollment(1), Enrollment(2))) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getObserveeEnrollments(any()) } returns expected + + val result = repository.getObserveeEnrollments(true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getObserveeEnrollments(true) } + coVerify(exactly = 0) { localDataSource.getObserveeEnrollments(any()) } + } + + @Test + fun `Return failed result for observee enrollments if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getObserveeEnrollments(any()) } returns DataResult.Fail() + + val result = repository.getObserveeEnrollments(false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return observee enrollments if offline`() = runTest { + val expected = DataResult.Success(listOf(Enrollment(1), Enrollment(2))) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getObserveeEnrollments(any()) } returns expected + + val result = repository.getObserveeEnrollments(true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getObserveeEnrollments(any()) } + coVerify(exactly = 1) { localDataSource.getObserveeEnrollments(true) } + } + + @Test + fun `Return submission if online`() = runTest { + val expected = DataResult.Success(Submission(1)) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getSingleSubmission(any(), any(), any(), any()) } returns expected + + val result = repository.getSingleSubmission(1, 1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getSingleSubmission(1, 1, 1, true) } + coVerify(exactly = 0) { localDataSource.getSingleSubmission(any(), any(), any(), any()) } + } + + @Test + fun `Return failed result for submission if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getSingleSubmission(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = repository.getSingleSubmission(1, 1, 1, false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return submission if offline`() = runTest { + val expected = DataResult.Success(Submission(1)) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getSingleSubmission(any(), any(), any(), any()) } returns expected + + val result = repository.getSingleSubmission(1, 1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getSingleSubmission(any(), any(), any(), any()) } + coVerify(exactly = 1) { localDataSource.getSingleSubmission(1, 1, 1, true) } + } + + @Test + fun `Return assignment if online`() = runTest { + val expected = DataResult.Success(Assignment(1)) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getAssignment(any(), any(), any()) } returns expected + + val result = repository.getAssignment(1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getAssignment(1, 1, true) } + coVerify(exactly = 0) { localDataSource.getAssignment(any(), any(), any()) } + } + + @Test + fun `Return failed result for assignment if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getAssignment(any(), any(), any()) } returns DataResult.Fail() + + val result = repository.getAssignment(1, 1, false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return assignment if offline`() = runTest { + val expected = DataResult.Success(Assignment(1)) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getAssignment(any(), any(), any()) } returns expected + + val result = repository.getAssignment(1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getAssignment(any(), any(), any()) } + coVerify(exactly = 1) { localDataSource.getAssignment(1, 1, true) } + } + + @Test + fun `Return external tool launch url if online`() = runTest { + val expected = DataResult.Success(LTITool(1)) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getExternalToolLaunchUrl(any(), any(), any(), any()) } returns expected + + val result = repository.getExternalToolLaunchUrl(1, 1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getExternalToolLaunchUrl(1, 1, 1, true) } + coVerify(exactly = 0) { localDataSource.getExternalToolLaunchUrl(any(), any(), any(), any()) } + } + + @Test + fun `Return failed result for external tool launch url if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getExternalToolLaunchUrl(any(), any(), any(), any()) } returns DataResult.Fail() + + val result = repository.getExternalToolLaunchUrl(1, 1, 1, false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return lti from authentication url if online`() = runTest { + val expected = DataResult.Success(LTITool(1)) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getLtiFromAuthenticationUrl(any(), any()) } returns expected + + val result = repository.getLtiFromAuthenticationUrl("url", true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getLtiFromAuthenticationUrl("url", true) } + coVerify(exactly = 0) { localDataSource.getLtiFromAuthenticationUrl(any(), any()) } + } + + @Test + fun `Return failed result for lti from authentication url if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getLtiFromAuthenticationUrl(any(), any()) } returns DataResult.Fail() + + val result = repository.getLtiFromAuthenticationUrl("url", false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return quiz if online`() = runTest { + val expected = DataResult.Success(Quiz(1)) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getQuiz(any(), any(), any()) } returns expected + + val result = repository.getQuiz(1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getQuiz(1, 1, true) } + coVerify(exactly = 0) { localDataSource.getQuiz(any(), any(), any()) } + } + + @Test + fun `Return failed result for quiz if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getQuiz(any(), any(), any()) } returns DataResult.Fail() + + val result = repository.getQuiz(1, 1, false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return quiz if offline`() = runTest { + val expected = DataResult.Success(Quiz(1)) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getQuiz(any(), any(), any()) } returns expected + + val result = repository.getQuiz(1, 1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getQuiz(any(), any(), any()) } + coVerify(exactly = 1) { localDataSource.getQuiz(1, 1, true) } + } + + @Test + fun `Return course features if online`() = runTest { + val expected = DataResult.Success(listOf("feature")) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getCourseFeatures(any(), any()) } returns expected + + val result = repository.getCourseFeatures(1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getCourseFeatures(1, true) } + coVerify(exactly = 0) { localDataSource.getCourseFeatures(any(), any()) } + } + + @Test + fun `Return failed result for course features if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getCourseFeatures(any(), any()) } returns DataResult.Fail() + + val result = repository.getCourseFeatures(1, false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return course features if offline`() = runTest { + val expected = DataResult.Success(listOf("feature")) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getCourseFeatures(any(), any()) } returns expected + + val result = repository.getCourseFeatures(1, true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getCourseFeatures(any(), any()) } + coVerify(exactly = 1) { localDataSource.getCourseFeatures(1, true) } + } + + @Test + fun `Load curse settings from local storage when device is offline`() = runTest { + coEvery { networkDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = false) + coEvery { localDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = true) + coEvery { networkStateProvider.isOnline() } returns false + + val result = repository.loadCourseSettings(1, true) + + Assert.assertTrue(result!!.restrictQuantitativeData) + } + + @Test + fun `Load curse settings from network when device is online`() = runTest { + coEvery { networkDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = false) + coEvery { localDataSource.loadCourseSettings(any(), any()) } returns CourseSettings(restrictQuantitativeData = true) + coEvery { networkStateProvider.isOnline() } returns true + + val result = repository.loadCourseSettings(1, true) + + Assert.assertFalse(result!!.restrictQuantitativeData) + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt index 959a28f7ff..625a803c43 100644 --- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsEffectHandlerTest.kt @@ -16,13 +16,12 @@ */ package com.instructure.student.test.conferences.conference_details -import com.instructure.canvasapi2.managers.ConferenceManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.DataResult -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsEffect import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsEffectHandler import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsEvent +import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsRepository import com.instructure.student.mobius.conferences.conference_details.ui.ConferenceDetailsView import com.spotify.mobius.functions.Consumer import io.mockk.* @@ -38,8 +37,10 @@ import org.junit.Test class ConferenceDetailsEffectHandlerTest : Assert() { private val testDispatcher = TestCoroutineDispatcher() private val view: ConferenceDetailsView = mockk(relaxed = true) - private val effectHandler = - ConferenceDetailsEffectHandler().apply { view = this@ConferenceDetailsEffectHandlerTest.view } + private val repository: ConferenceDetailsRepository = mockk(relaxed = true) + private val effectHandler = ConferenceDetailsEffectHandler(repository).apply { + view = this@ConferenceDetailsEffectHandlerTest.view + } private val eventConsumer: Consumer = mockk(relaxed = true) private val connection = effectHandler.connect(eventConsumer) @@ -105,8 +106,7 @@ class ConferenceDetailsEffectHandlerTest : Assert() { val authenticate = true // Mock API - mockkStatic("com.instructure.canvasapi2.utils.weave.AwaitApiKt") - coEvery { awaitApi(any()) } returns AuthenticatedSession(sessionUrl) + coEvery { repository.getAuthenticatedSession(any()) } returns AuthenticatedSession(sessionUrl) connection.accept(ConferenceDetailsEffect.JoinConference(url, authenticate)) @@ -114,7 +114,7 @@ class ConferenceDetailsEffectHandlerTest : Assert() { verify { view.launchUrl(sessionUrl) } // Should call API - coVerify { awaitApi(any()) } + coVerify { repository.getAuthenticatedSession(url) } // Advance the clock to skip delay advanceUntilIdle() @@ -133,17 +133,14 @@ class ConferenceDetailsEffectHandlerTest : Assert() { val apiResult = DataResult.Success(emptyList()) // Mock API - mockkObject(ConferenceManager) - every { ConferenceManager.getConferencesForContextAsync(any(), any()) } returns mockk { - coEvery { await() } returns apiResult - } + coEvery { repository.getConferencesForContext(any(), any()) } returns apiResult connection.accept(ConferenceDetailsEffect.RefreshData(canvasContext)) - verify { ConferenceManager.getConferencesForContextAsync(canvasContext, true) } + coVerify { repository.getConferencesForContext(canvasContext, true) } verify { eventConsumer.accept(ConferenceDetailsEvent.RefreshFinished(apiResult)) } - confirmVerified(ConferenceManager) + confirmVerified(repository) confirmVerified(eventConsumer) } diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt new file mode 100644 index 0000000000..3a897a34ed --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsLocalDataSourceTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.conferences.conference_details + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.ConferenceFacade +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ConferenceDetailsLocalDataSourceTest { + + private val conferenceFacade: ConferenceFacade = mockk(relaxed = true) + private lateinit var localDataSource: ConferenceDetailsLocalDataSource + + @Before + fun setup() { + localDataSource = ConferenceDetailsLocalDataSource(conferenceFacade) + } + + @Test + fun `Return conferences api model`() = runTest { + val conferences = listOf(Conference(id = 1, conferenceKey = "key 1"), Conference(id = 2, conferenceKey = "key 2")) + val expected = DataResult.Success(conferences) + + coEvery { conferenceFacade.getConferencesByCourseId(any()) } returns conferences + + val result = localDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + conferenceFacade.getConferencesByCourseId(1) + } + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt new file mode 100644 index 0000000000..9f87aeaf4f --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsNetworkDataSourceTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.conferences.conference_details + +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.ConferenceList +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ConferenceDetailsNetworkDataSourceTest { + + private val conferencesApi: ConferencesApi.ConferencesInterface = mockk(relaxed = true) + private val oAuthApi: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private lateinit var networkDataSource: ConferenceDetailsNetworkDataSource + + @Before + fun setup() { + networkDataSource = ConferenceDetailsNetworkDataSource(conferencesApi, oAuthApi) + } + + @Test + fun `Return conferences list api model`() = runTest { + val expected = listOf(Conference(1), Conference(2)) + coEvery { conferencesApi.getConferencesForContext(any(), any()) } returns DataResult.Success(ConferenceList(expected)) + + val result = networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + conferencesApi.getConferencesForContext( + CanvasContext.emptyCourseContext(1).toAPIString().drop(1), + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + ) + } + } + + @Test + fun `Depaginate conferences`() = runTest { + val page1 = ConferenceList(listOf(Conference(1), Conference(2), Conference(3))) + val page2 = ConferenceList(listOf(Conference(4), Conference(5), Conference(6))) + val page3 = ConferenceList(listOf(Conference(7), Conference(8), Conference(9))) + + coEvery { conferencesApi.getConferencesForContext(any(), any()) } returns DataResult.Success( + page1, + linkHeaders = LinkHeaders(nextUrl = "page_2_url") + ) + + coEvery { conferencesApi.getNextPage("page_2_url", any()) } returns DataResult.Success( + page2, + linkHeaders = LinkHeaders(nextUrl = "page_3_url") + ) + + coEvery { conferencesApi.getNextPage("page_3_url", any()) } returns DataResult.Success(page3) + + val result = networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Success(page1.conferences + page2.conferences + page3.conferences), result) + coVerify(exactly = 1) { + conferencesApi.getConferencesForContext( + CanvasContext.emptyCourseContext(1).toAPIString().drop(1), + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + ) + conferencesApi.getNextPage("page_2_url", RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true)) + conferencesApi.getNextPage("page_3_url", RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed result if conferences call fails`() = runTest { + val expected = DataResult.Fail() + coEvery { conferencesApi.getConferencesForContext(any(), any()) } returns expected + + val result = networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + conferencesApi.getConferencesForContext( + CanvasContext.emptyCourseContext(1).toAPIString().drop(1), + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + ) + } + } + + @Test + fun `Return authenticated session api model`() = runTest { + val expected = AuthenticatedSession("url") + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Success(expected) + + val result = networkDataSource.getAuthenticatedSession("targetUrl") + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { oAuthApi.getAuthenticatedSession("targetUrl", RestParams(isForceReadFromNetwork = true)) } + } + + @Test(expected = IllegalStateException::class) + fun `Throws exception if authenticated session call fails`() = runTest { + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + networkDataSource.getAuthenticatedSession("targetUrl") + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt new file mode 100644 index 0000000000..8324672d8e --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_details/ConferenceDetailsRepositoryTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.conferences.conference_details + +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_details.ConferenceDetailsRepository +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsLocalDataSource +import com.instructure.student.mobius.conferences.conference_details.datasource.ConferenceDetailsNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ConferenceDetailsRepositoryTest { + + private val localDataSource: ConferenceDetailsLocalDataSource = mockk(relaxed = true) + private val networkDataSource: ConferenceDetailsNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private lateinit var repository: ConferenceDetailsRepository + + @Before + fun setUp() { + repository = ConferenceDetailsRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Return conference list if online`() = runTest { + val expected = DataResult.Success(listOf(Conference(1), Conference(2))) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getConferencesForContext(any(), any()) } returns expected + + val result = repository.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) } + coVerify(exactly = 0) { localDataSource.getConferencesForContext(any(), any()) } + } + + @Test + fun `Return failed result for conference list if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getConferencesForContext(any(), any()) } returns DataResult.Fail() + + val result = repository.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return conference list if offline`() = runTest { + val expected = DataResult.Success(listOf(Conference(1), Conference(2))) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getConferencesForContext(any(), any()) } returns expected + + val result = repository.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getConferencesForContext(any(), any()) } + coVerify(exactly = 1) { localDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) } + } + + @Test + fun `Return authenticated session if online`() = runTest { + val expected = AuthenticatedSession("sessionUrl") + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getAuthenticatedSession(any()) } returns expected + + val result = repository.getAuthenticatedSession("targetUrl") + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getAuthenticatedSession("targetUrl") } + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt index d2c99ec901..ae9b2b0cec 100644 --- a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListEffectHandlerTest.kt @@ -16,13 +16,12 @@ */ package com.instructure.student.test.conferences.conference_list -import com.instructure.canvasapi2.managers.ConferenceManager import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.DataResult -import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.student.mobius.conferences.conference_list.ConferenceListEffect import com.instructure.student.mobius.conferences.conference_list.ConferenceListEffectHandler import com.instructure.student.mobius.conferences.conference_list.ConferenceListEvent +import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListView import com.spotify.mobius.functions.Consumer import io.mockk.* @@ -38,8 +37,10 @@ import org.junit.Test class ConferenceListEffectHandlerTest : Assert() { private val testDispatcher = TestCoroutineDispatcher() private val view: ConferenceListView = mockk(relaxed = true) - private val effectHandler = - ConferenceListEffectHandler().apply { view = this@ConferenceListEffectHandlerTest.view } + private val repository: ConferenceListRepository = mockk(relaxed = true) + private val effectHandler = ConferenceListEffectHandler(repository).apply { + view = this@ConferenceListEffectHandlerTest.view + } private val eventConsumer: Consumer = mockk(relaxed = true) private val connection = effectHandler.connect(eventConsumer) @@ -66,17 +67,14 @@ class ConferenceListEffectHandlerTest : Assert() { val apiResult = DataResult.Success>(emptyList()) // Mock API - mockkObject(ConferenceManager) - every { ConferenceManager.getConferencesForContextAsync(any(), any()) } returns mockk { - coEvery { await() } returns apiResult - } + coEvery { repository.getConferencesForContext(any(), any()) } returns apiResult connection.accept(ConferenceListEffect.LoadData(canvasContext, refresh)) - verify { ConferenceManager.getConferencesForContextAsync(canvasContext, refresh) } + coVerify { repository.getConferencesForContext(canvasContext, refresh) } verify { eventConsumer.accept(ConferenceListEvent.DataLoaded(apiResult)) } - confirmVerified(ConferenceManager) + confirmVerified(repository) confirmVerified(eventConsumer) } @@ -86,8 +84,7 @@ class ConferenceListEffectHandlerTest : Assert() { val sessionUrl = "session-url" // Mock API - mockkStatic("com.instructure.canvasapi2.utils.weave.AwaitApiKt") - coEvery { awaitApi(any()) } returns AuthenticatedSession(sessionUrl) + coEvery { repository.getAuthenticatedSession(any()) } returns AuthenticatedSession(sessionUrl) connection.accept(ConferenceListEffect.LaunchInBrowser(url)) @@ -95,7 +92,7 @@ class ConferenceListEffectHandlerTest : Assert() { verify { view.launchUrl(sessionUrl) } // Should call API - coVerify { awaitApi(any()) } + coVerify { repository.getAuthenticatedSession(url) } // Advance the clock to skip delay advanceUntilIdle() diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt new file mode 100644 index 0000000000..5670a532ff --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListLocalDataSourceTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.conferences.conference_list + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.facade.ConferenceFacade +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ConferenceListLocalDataSourceTest { + + private val conferenceFacade: ConferenceFacade = mockk(relaxed = true) + private lateinit var localDataSource: ConferenceListLocalDataSource + + @Before + fun setup() { + localDataSource = ConferenceListLocalDataSource(conferenceFacade) + } + + @Test + fun `Return conferences api model`() = runTest { + val conferences = listOf(Conference(id = 1, conferenceKey = "key 1"), Conference(id = 2, conferenceKey = "key 2")) + val expected = DataResult.Success(conferences) + + coEvery { conferenceFacade.getConferencesByCourseId(any()) } returns conferences + + val result = localDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + conferenceFacade.getConferencesByCourseId(1) + } + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt new file mode 100644 index 0000000000..cfc9cfb2db --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListNetworkDataSourceTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.conferences.conference_list + +import com.instructure.canvasapi2.apis.ConferencesApi +import com.instructure.canvasapi2.apis.OAuthAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.ConferenceList +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ConferenceListNetworkDataSourceTest { + + private val conferencesApi: ConferencesApi.ConferencesInterface = mockk(relaxed = true) + private val oAuthApi: OAuthAPI.OAuthInterface = mockk(relaxed = true) + private lateinit var networkDataSource: ConferenceListNetworkDataSource + + @Before + fun setup() { + networkDataSource = ConferenceListNetworkDataSource(conferencesApi, oAuthApi) + } + + @Test + fun `Return conferences list api model`() = runTest { + val expected = listOf(Conference(1), Conference(2)) + coEvery { conferencesApi.getConferencesForContext(any(), any()) } returns DataResult.Success(ConferenceList(expected)) + + val result = networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + conferencesApi.getConferencesForContext( + CanvasContext.emptyCourseContext(1).toAPIString().drop(1), + RestParams(isForceReadFromNetwork = true) + ) + } + } + + @Test + fun `Depaginate conferences`() = runTest { + val page1 = ConferenceList(listOf(Conference(1), Conference(2), Conference(3))) + val page2 = ConferenceList(listOf(Conference(4), Conference(5), Conference(6))) + val page3 = ConferenceList(listOf(Conference(7), Conference(8), Conference(9))) + + coEvery { conferencesApi.getConferencesForContext(any(), any()) } returns DataResult.Success( + page1, + linkHeaders = LinkHeaders(nextUrl = "page_2_url") + ) + + coEvery { conferencesApi.getNextPage("page_2_url", any()) } returns DataResult.Success( + page2, + linkHeaders = LinkHeaders(nextUrl = "page_3_url") + ) + + coEvery { conferencesApi.getNextPage("page_3_url", any()) } returns DataResult.Success(page3) + + val result = networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(DataResult.Success(page1.conferences + page2.conferences + page3.conferences), result) + coVerify(exactly = 1) { + conferencesApi.getConferencesForContext( + CanvasContext.emptyCourseContext(1).toAPIString().drop(1), + RestParams(isForceReadFromNetwork = true) + ) + conferencesApi.getNextPage("page_2_url", RestParams(isForceReadFromNetwork = true)) + conferencesApi.getNextPage("page_3_url", RestParams(isForceReadFromNetwork = true)) + } + } + + @Test + fun `Return failed result if conferences call fails`() = runTest { + val expected = DataResult.Fail() + coEvery { conferencesApi.getConferencesForContext(any(), any()) } returns expected + + val result = networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { + conferencesApi.getConferencesForContext( + CanvasContext.emptyCourseContext(1).toAPIString().drop(1), + RestParams(isForceReadFromNetwork = true) + ) + } + } + + @Test + fun `Return authenticated session api model`() = runTest { + val expected = AuthenticatedSession("url") + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Success(expected) + + val result = networkDataSource.getAuthenticatedSession("targetUrl") + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { oAuthApi.getAuthenticatedSession("targetUrl", RestParams(isForceReadFromNetwork = true)) } + } + + @Test(expected = IllegalStateException::class) + fun `Throws exception if authenticated session call fails`() = runTest { + coEvery { oAuthApi.getAuthenticatedSession(any(), any()) } returns DataResult.Fail() + + networkDataSource.getAuthenticatedSession("targetUrl") + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt new file mode 100644 index 0000000000..27678507ff --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/conferences/conference_list/ConferenceListRepositoryTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.test.conferences.conference_list + +import com.instructure.canvasapi2.models.AuthenticatedSession +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListLocalDataSource +import com.instructure.student.mobius.conferences.conference_list.datasource.ConferenceListNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class ConferenceListRepositoryTest { + + private val localDataSource: ConferenceListLocalDataSource = mockk(relaxed = true) + private val networkDataSource: ConferenceListNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private lateinit var repository: ConferenceListRepository + + @Before + fun setUp() { + repository = ConferenceListRepository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Return conference list if online`() = runTest { + val expected = DataResult.Success(listOf(Conference(1), Conference(2))) + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getConferencesForContext(any(), any()) } returns expected + + val result = repository.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), true) } + coVerify(exactly = 0) { localDataSource.getConferencesForContext(any(), any()) } + } + + @Test + fun `Return failed result for conference list if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getConferencesForContext(any(), any()) } returns DataResult.Fail() + + val result = repository.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) + + TestCase.assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return conference list if offline`() = runTest { + val expected = DataResult.Success(listOf(Conference(1), Conference(2))) + + every { networkStateProvider.isOnline() } returns false + + coEvery { localDataSource.getConferencesForContext(any(), any()) } returns expected + + val result = repository.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) + + TestCase.assertEquals(expected, result) + coVerify(exactly = 0) { networkDataSource.getConferencesForContext(any(), any()) } + coVerify(exactly = 1) { localDataSource.getConferencesForContext(CanvasContext.emptyCourseContext(1), false) } + } + + @Test + fun `Return authenticated session if online`() = runTest { + val expected = AuthenticatedSession("sessionUrl") + + every { networkStateProvider.isOnline() } returns true + + coEvery { networkDataSource.getAuthenticatedSession(any()) } returns expected + + val result = repository.getAuthenticatedSession("targetUrl") + + TestCase.assertEquals(expected, result) + coVerify(exactly = 1) { networkDataSource.getAuthenticatedSession("targetUrl") } + } +} diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt index a7322078a3..fe6928bb8c 100644 --- a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusEffectHandlerTest.kt @@ -16,8 +16,6 @@ package com.instructure.student.test.syllabus import com.instructure.canvasapi2.apis.CalendarEventAPI -import com.instructure.canvasapi2.managers.CalendarEventManager -import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.CourseSettings @@ -27,9 +25,13 @@ import com.instructure.canvasapi2.utils.toApiString import com.instructure.student.mobius.syllabus.SyllabusEffect import com.instructure.student.mobius.syllabus.SyllabusEffectHandler import com.instructure.student.mobius.syllabus.SyllabusEvent +import com.instructure.student.mobius.syllabus.SyllabusRepository import com.instructure.student.mobius.syllabus.ui.SyllabusView import com.spotify.mobius.functions.Consumer -import io.mockk.* +import io.mockk.coEvery +import io.mockk.confirmVerified +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher @@ -42,8 +44,9 @@ import java.util.concurrent.Executors class SyllabusEffectHandlerTest : Assert() { private val view: SyllabusView = mockk(relaxed = true) + private val syllabusRepository: SyllabusRepository = mockk(relaxed = true) private val effectHandler = - SyllabusEffectHandler().apply { view = this@SyllabusEffectHandlerTest.view } + SyllabusEffectHandler(syllabusRepository).apply { view = this@SyllabusEffectHandlerTest.view } private val eventConsumer: Consumer = mockk(relaxed = true) private val connection = effectHandler.connect(eventConsumer) @@ -66,13 +69,9 @@ class SyllabusEffectHandlerTest : Assert() { DataResult.Fail() ) - mockkObject(CourseManager) - every { CourseManager.getCourseWithSyllabusAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } - every { CourseManager.getCourseSettingsAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) - } + coEvery { syllabusRepository.getCourseWithSyllabus(any(), any()) } returns DataResult.Fail() + + coEvery { syllabusRepository.getCourseSettings(any(), any()) } returns CourseSettings(courseSummary = true) connection.accept(SyllabusEffect.LoadData(courseId, false)) @@ -91,18 +90,20 @@ class SyllabusEffectHandlerTest : Assert() { DataResult.Fail() ) - mockkObject(CourseManager) - every { CourseManager.getCourseWithSyllabusAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(course) - } - every { CourseManager.getCourseSettingsAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) - } + coEvery { syllabusRepository.getCourseWithSyllabus(any(), any()) } returns DataResult.Success(course) - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), any(), any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { syllabusRepository.getCourseSettings(any(), any()) } returns CourseSettings(courseSummary = true) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() connection.accept(SyllabusEffect.LoadData(courseId, false)) @@ -122,13 +123,15 @@ class SyllabusEffectHandlerTest : Assert() { ScheduleItem( itemId = it.toString(), itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, - startAt = Date(now + (1000 * it)).toApiString()) + startAt = Date(now + (1000 * it)).toApiString() + ) } val calendarEvents = List(itemCount) { ScheduleItem( itemId = (it + assignments.size).toString(), itemType = ScheduleItem.Type.TYPE_CALENDAR, - startAt = Date(now + (1000 * it)).toApiString()) + startAt = Date(now + (1000 * it)).toApiString() + ) } val sortedEvents = mutableListOf() for (i in 0 until itemCount) { @@ -141,21 +144,31 @@ class SyllabusEffectHandlerTest : Assert() { DataResult.Success(sortedEvents) ) - mockkObject(CourseManager) - every { CourseManager.getCourseWithSyllabusAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(course) - } - every { CourseManager.getCourseSettingsAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) - } - - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { syllabusRepository.getCourseWithSyllabus(any(), any()) } returns DataResult.Success(course) + + coEvery { syllabusRepository.getCourseSettings(any(), any()) } returns CourseSettings(courseSummary = true) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(assignments) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.CALENDAR, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(calendarEvents) connection.accept(SyllabusEffect.LoadData(courseId, false)) @@ -178,21 +191,31 @@ class SyllabusEffectHandlerTest : Assert() { DataResult.Success(assignments) ) - mockkObject(CourseManager) - every { CourseManager.getCourseWithSyllabusAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(course) - } - every { CourseManager.getCourseSettingsAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) - } - - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } + coEvery { syllabusRepository.getCourseWithSyllabus(courseId, false) } returns DataResult.Success(course) + + coEvery { syllabusRepository.getCourseSettings(courseId, false) } returns CourseSettings(courseSummary = true) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(assignments) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.CALENDAR, + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() connection.accept(SyllabusEffect.LoadData(courseId, false)) @@ -215,21 +238,31 @@ class SyllabusEffectHandlerTest : Assert() { DataResult.Success(calendarEvents) ) - mockkObject(CourseManager) - every { CourseManager.getCourseWithSyllabusAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(course) - } - every { CourseManager.getCourseSettingsAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = true)) - } - - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Fail() - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { syllabusRepository.getCourseWithSyllabus(courseId, false) } returns DataResult.Success(course) + + coEvery { syllabusRepository.getCourseSettings(courseId, false) } returns CourseSettings(courseSummary = true) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.CALENDAR, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(calendarEvents) connection.accept(SyllabusEffect.LoadData(courseId, false)) @@ -249,13 +282,15 @@ class SyllabusEffectHandlerTest : Assert() { ScheduleItem( itemId = it.toString(), itemType = ScheduleItem.Type.TYPE_ASSIGNMENT, - startAt = Date(now + (1000 * it)).toApiString()) + startAt = Date(now + (1000 * it)).toApiString() + ) } val calendarEvents = List(itemCount) { ScheduleItem( itemId = (it + assignments.size).toString(), itemType = ScheduleItem.Type.TYPE_CALENDAR, - startAt = Date(now + (1000 * it)).toApiString()) + startAt = Date(now + (1000 * it)).toApiString() + ) } val sortedEvents = mutableListOf() for (i in 0 until itemCount) { @@ -268,21 +303,31 @@ class SyllabusEffectHandlerTest : Assert() { DataResult.Success(emptyList()) ) - mockkObject(CourseManager) - every { CourseManager.getCourseSettingsAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(CourseSettings(courseSummary = false)) - } - every { CourseManager.getCourseWithSyllabusAsync(courseId, false) } returns mockk { - coEvery { await() } returns DataResult.Success(course) - } - - mockkObject(CalendarEventManager) - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.ASSIGNMENT, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(assignments) - } - every { CalendarEventManager.getCalendarEventsExhaustiveAsync(any(), CalendarEventAPI.CalendarEventType.CALENDAR, any(), any(), any(), any()) } returns mockk { - coEvery { await() } returns DataResult.Success(calendarEvents) - } + coEvery { syllabusRepository.getCourseSettings(courseId, false) } returns CourseSettings(courseSummary = false) + + coEvery { syllabusRepository.getCourseWithSyllabus(courseId, false) } returns DataResult.Success(course) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(assignments) + + coEvery { + syllabusRepository.getCalendarEvents( + any(), + CalendarEventAPI.CalendarEventType.CALENDAR, + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(calendarEvents) connection.accept(SyllabusEffect.LoadData(courseId, false)) diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt new file mode 100644 index 0000000000..563ed2a5ee --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusLocalDataSourceTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.test.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.facade.CourseFacade +import com.instructure.pandautils.room.offline.facade.ScheduleItemFacade +import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SyllabusLocalDataSourceTest { + + private val courseSettingsDao: CourseSettingsDao = mockk(relaxed = true) + private val courseFacade: CourseFacade = mockk(relaxed = true) + private val scheduleItemFacade: ScheduleItemFacade = mockk(relaxed = true) + + private lateinit var syllabusLocalDataSource: SyllabusLocalDataSource + + @Before + fun setup() { + syllabusLocalDataSource = SyllabusLocalDataSource(courseSettingsDao, courseFacade, scheduleItemFacade) + } + + @Test + fun `Return course settings api model`() = runTest { + val expected = CourseSettings(courseSummary = true) + coEvery { courseSettingsDao.findByCourseId(any()) } returns CourseSettingsEntity(1L, true, false) + + val result = syllabusLocalDataSource.getCourseSettings(1L, false) + + assertEquals(expected, result) + coVerify(exactly = 1) { + courseSettingsDao.findByCourseId(1L) + } + } + + @Test + fun `Return course api model`() = runTest { + val expected = Course(id = 1L, syllabusBody = "Syllabus Body") + coEvery { courseFacade.getCourseById(any()) } returns expected + + val result = syllabusLocalDataSource.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + courseFacade.getCourseById(1L) + } + } + + @Test + fun `Return failed result if the course is missing`() = runTest { + coEvery { courseFacade.getCourseById(any()) } returns null + + val result = syllabusLocalDataSource.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return calendar events`() = runTest { + val expected = listOf(ScheduleItem(itemId = "event_1"), ScheduleItem(itemId = "event_2")) + + coEvery { scheduleItemFacade.findByItemType(any(), any()) } returns expected + + val result = syllabusLocalDataSource.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + null, + null, + listOf("course_1"), + false + ) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + scheduleItemFacade.findByItemType(listOf("course_1"), CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName) + } + } + + @Test + fun `Return failed data result on calendar event error`() = runTest { + coEvery { scheduleItemFacade.findByItemType(any(), any()) } throws Exception() + + val result = syllabusLocalDataSource.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT, + null, + null, + listOf("course_1"), + false + ) + + assertEquals(DataResult.Fail(), result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt new file mode 100644 index 0000000000..2b98c7787b --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusNetworkDataSourceTest.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.test.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import com.instructure.student.mobius.syllabus.datasource.SyllabusNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SyllabusNetworkDataSourceTest { + + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface = mockk(relaxed = true) + + private lateinit var syllabusNetworkDataSource: SyllabusNetworkDataSource + + @Before + fun setup() { + syllabusNetworkDataSource = SyllabusNetworkDataSource(courseApi, calendarEventApi) + } + + @Test + fun `Return course settings`() = runTest { + val expected = CourseSettings(courseSummary = false) + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Success(expected) + + val result = syllabusNetworkDataSource.getCourseSettings(1L, false) + + assertEquals(expected, result) + coVerify(exactly = 1) { courseApi.getCourseSettings(1L, RestParams(isForceReadFromNetwork = false)) } + } + + @Test + fun `Return null if course settings fails`() = runTest { + coEvery { courseApi.getCourseSettings(any(), any()) } returns DataResult.Fail() + + val result = syllabusNetworkDataSource.getCourseSettings(1L, false) + + assertNull(result) + } + + @Test + fun `Return course with syllabus`() = runTest { + val expected = Course(id = 1L, syllabusBody = "Syllabus") + coEvery { courseApi.getCourseWithSyllabus(any(), any()) } returns DataResult.Success(expected) + + val result = syllabusNetworkDataSource.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { courseApi.getCourseWithSyllabus(1L, RestParams(isForceReadFromNetwork = false)) } + } + + @Test + fun `Return failed data result if course call fails`() = runTest { + coEvery { courseApi.getCourseWithSyllabus(any(), any()) } returns DataResult.Fail() + + val result = syllabusNetworkDataSource.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return calendar events`() = runTest { + val expected = listOf(ScheduleItem(itemId = "event_1"), ScheduleItem(itemId = "event_2")) + + coEvery { + calendarEventApi.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = syllabusNetworkDataSource.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR, + null, + null, + listOf("course_1"), + false + ) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + null, + null, + listOf("course_1"), + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = false) + ) + } + } + + @Test + fun `Depaginate calendar events`() = runTest { + val page1 = listOf(ScheduleItem(itemId = "event_1"), ScheduleItem(itemId = "event_2")) + val page2 = listOf(ScheduleItem(itemId = "event_3"), ScheduleItem(itemId = "event_4")) + + coEvery { + calendarEventApi.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(page1, linkHeaders = LinkHeaders(nextUrl = "next_url")) + + coEvery { calendarEventApi.next(any(), any()) } returns DataResult.Success(page2) + + val result = syllabusNetworkDataSource.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR, + null, + null, + listOf("course_1"), + false + ) + + assertEquals(DataResult.Success(page1 + page2), result) + coVerify(exactly = 1) { + calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + null, + null, + listOf("course_1"), + RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = false) + ) + calendarEventApi.next("next_url", RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = false)) + } + } + + @Test + fun `Return failed data result if calender events fail`() = runTest { + coEvery { calendarEventApi.getCalendarEvents(any(), any(), any(), any(), any(), any()) } returns DataResult.Fail() + + val result = syllabusNetworkDataSource.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR, + null, + null, + listOf("course_1"), + false + ) + + assertEquals(DataResult.Fail(), result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt new file mode 100644 index 0000000000..34d6b41f47 --- /dev/null +++ b/apps/student/src/test/java/com/instructure/student/test/syllabus/SyllabusRepositoryTest.kt @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.student.test.syllabus + +import com.instructure.canvasapi2.apis.CalendarEventAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.student.mobius.conferences.conference_list.ConferenceListRepository +import com.instructure.student.mobius.syllabus.SyllabusRepository +import com.instructure.student.mobius.syllabus.datasource.SyllabusLocalDataSource +import com.instructure.student.mobius.syllabus.datasource.SyllabusNetworkDataSource +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SyllabusRepositoryTest { + + private val syllabusLocalDataSource: SyllabusLocalDataSource = mockk(relaxed = true) + private val syllabusNetworkDataSource: SyllabusNetworkDataSource = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) + private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) + + private lateinit var repository: SyllabusRepository + + @Before + fun setUp() { + repository = SyllabusRepository(syllabusLocalDataSource, syllabusNetworkDataSource, networkStateProvider, featureFlagProvider) + coEvery { featureFlagProvider.offlineEnabled() } returns true + } + + @Test + fun `Return course settings if online`() = runTest { + val expected = CourseSettings(false) + + every { networkStateProvider.isOnline() } returns true + + coEvery { syllabusNetworkDataSource.getCourseSettings(any(), any()) } returns expected + + val result = repository.getCourseSettings(1L, false) + + assertEquals(expected, result) + coVerify(exactly = 1) { syllabusNetworkDataSource.getCourseSettings(1L, false) } + coVerify(exactly = 0) { syllabusLocalDataSource.getCourseSettings(any(), any()) } + } + + @Test + fun `Return course settings if offline`() = runTest { + val expected = CourseSettings(false) + + every { networkStateProvider.isOnline() } returns false + + coEvery { syllabusLocalDataSource.getCourseSettings(any(), any()) } returns expected + + val result = repository.getCourseSettings(1L, false) + + assertEquals(expected, result) + coVerify(exactly = 0) { syllabusNetworkDataSource.getCourseSettings(any(), any()) } + coVerify(exactly = 1) { syllabusLocalDataSource.getCourseSettings(1L, false) } + } + + @Test + fun `Return course with syllabus when online`() = runTest { + val expected = Course(1L, syllabusBody = "Syllabus body") + + every { networkStateProvider.isOnline() } returns true + + coEvery { syllabusNetworkDataSource.getCourseWithSyllabus(any(), any()) } returns DataResult.Success(expected) + + val result = repository.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusNetworkDataSource.getCourseWithSyllabus(1L, false) } + coVerify(exactly = 0) { syllabusLocalDataSource.getCourseWithSyllabus(any(), any()) } + } + + @Test + fun `Return course with syllabus when offline`() = runTest { + val expected = Course(1L, syllabusBody = "Syllabus body") + + every { networkStateProvider.isOnline() } returns false + + coEvery { syllabusLocalDataSource.getCourseWithSyllabus(any(), any()) } returns DataResult.Success(expected) + + val result = repository.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusLocalDataSource.getCourseWithSyllabus(1L, false) } + coVerify(exactly = 0) { syllabusNetworkDataSource.getCourseWithSyllabus(any(), any()) } + } + + @Test + fun `Return failed result for course if network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { syllabusNetworkDataSource.getCourseWithSyllabus(any(), any()) } returns DataResult.Fail() + + val result = repository.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return failed result for course if db error`() = runTest { + every { networkStateProvider.isOnline() } returns false + + coEvery { syllabusLocalDataSource.getCourseWithSyllabus(any(), any()) } returns DataResult.Fail() + + val result = repository.getCourseWithSyllabus(1L, false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return events when online`() = runTest { + val expected = listOf( + ScheduleItem("assignment_1", type = "assignment"), + ScheduleItem("assignment_2", type = "assignment") + ) + + every { networkStateProvider.isOnline() } returns true + + coEvery { + syllabusNetworkDataSource.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, listOf("course_1"), false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusNetworkDataSource.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, listOf("course_1"), false) } + coVerify(exactly = 0) { syllabusLocalDataSource.getCalendarEvents(any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Return events when offline`() = runTest { + val expected = listOf( + ScheduleItem("assignment_1", type = "assignment"), + ScheduleItem("assignment_2", type = "assignment") + ) + + every { networkStateProvider.isOnline() } returns false + + coEvery { + syllabusLocalDataSource.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(expected) + + val result = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, listOf("course_1"), false) + + assertEquals(DataResult.Success(expected), result) + coVerify(exactly = 1) { syllabusLocalDataSource.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, listOf("course_1"), false) } + coVerify(exactly = 0) { syllabusNetworkDataSource.getCalendarEvents(any(), any(), any(), any(), any(), any()) } + } + + @Test + fun `Return failed result for events on network error`() = runTest { + every { networkStateProvider.isOnline() } returns true + + coEvery { + syllabusNetworkDataSource.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + val result = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, listOf("course_1"), false) + + assertEquals(DataResult.Fail(), result) + } + + @Test + fun `Return failed result for events on db error`() = runTest { + every { networkStateProvider.isOnline() } returns false + + coEvery { + syllabusLocalDataSource.getCalendarEvents( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns DataResult.Fail() + + val result = repository.getCalendarEvents(true, CalendarEventAPI.CalendarEventType.ASSIGNMENT, null, null, listOf("course_1"), false) + + assertEquals(DataResult.Fail(), result) + } +} \ No newline at end of file diff --git a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt index 26f2f7161a..cf5fd6ed3c 100644 --- a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt @@ -17,16 +17,24 @@ package com.instructure.student.test.util +import android.content.Context import android.os.Bundle import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.models.ModuleObject -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.canvasapi2.models.Tab +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.discussion.details.DiscussionDetailsFragment +import com.instructure.student.features.files.details.FileDetailsFragment +import com.instructure.student.features.modules.progression.ModuleQuizDecider +import com.instructure.student.features.modules.progression.NotAvailableOfflineFragment +import com.instructure.student.features.modules.util.ModuleUtility +import com.instructure.student.features.pages.details.PageDetailsFragment import com.instructure.student.fragment.* import com.instructure.student.util.Const -import com.instructure.student.util.ModuleUtility +import io.mockk.mockk import junit.framework.TestCase import org.junit.Test import org.junit.runner.RunWith @@ -34,6 +42,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ModuleUtilityTest : TestCase() { + private val context = mockk(relaxed = true) + @Test fun testGetFragment_file() { val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789" @@ -53,6 +63,7 @@ class ModuleUtilityTest : TestCase() { var expectedBundle = Bundle() expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) expectedBundle.putString(Const.FILE_URL, expectedUrl) + expectedBundle.putInt(Const.FILE_ID, 0) expectedBundle.putLong(Const.ITEM_ID, moduleItem.id) expectedBundle.putParcelable(com.instructure.pandautils.utils.Const.MODULE_OBJECT, moduleObject) @@ -67,6 +78,7 @@ class ModuleUtilityTest : TestCase() { expectedBundle = Bundle() expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) expectedBundle.putString(Const.FILE_URL, expectedUrl) + expectedBundle.putInt(Const.FILE_ID, 0) parentFragment = callGetFragment(moduleItem, course, moduleObject) TestCase.assertNotNull(parentFragment) TestCase.assertEquals(FileDetailsFragment::class.java, parentFragment!!.javaClass) @@ -74,6 +86,26 @@ class ModuleUtilityTest : TestCase() { } + @Test + fun testGetFragment_fileOfflineNotAvailable() { + val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789" + val moduleItem = ModuleItem( + id = 4567, + type = "File", + url = url + ) + + val moduleObject: ModuleObject = ModuleObject( + id = 1234 + ) + + val course = Course() + + val filDetailsFragment = callGetFragment(moduleItem, course, moduleObject, isOnline = false) + TestCase.assertNotNull(filDetailsFragment) + TestCase.assertEquals(NotAvailableOfflineFragment::class.java, filDetailsFragment!!.javaClass) + } + @Test fun testGetFragment_page() { val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world" @@ -96,6 +128,45 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } + @Test + fun testGetFragment_page_offlineSynced() { + val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world" + val moduleItem = ModuleItem( + id = 4567, + type = "Page", + url = url, + title = "hello-world" + ) + + val course = Course() + val expectedBundle = Bundle() + expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) + expectedBundle.putString(PageDetailsFragment.PAGE_NAME, "hello-world") + expectedBundle.putBoolean(PageDetailsFragment.NAVIGATED_FROM_MODULES, false) + + val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.PAGES_ID)) + TestCase.assertNotNull(parentFragment) + TestCase.assertEquals(PageDetailsFragment::class.java, parentFragment!!.javaClass) + TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + } + + @Test + fun testGetFragment_page_offlineNotSynced() { + val url = "https://mobile.canvas.net/api/v1/courses/222/pages/hello-world" + val moduleItem = ModuleItem( + id = 4567, + type = "Page", + url = url, + title = "hello-world" + ) + + val course = Course() + + val fragment = callGetFragment(moduleItem, course, null, isOnline = false) + TestCase.assertNotNull(fragment) + TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + } + @Test fun testGetFragment_assignment() { val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789" @@ -116,6 +187,42 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } + @Test + fun testGetFragment_assignment_offlineSynced() { + val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789" + val moduleItem = ModuleItem( + id = 4567, + type = "Assignment", + url = url + ) + + val course = Course() + val expectedBundle = Bundle() + expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) + expectedBundle.putLong(Const.ASSIGNMENT_ID, 123456789) + + val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.ASSIGNMENTS_ID)) + TestCase.assertNotNull(parentFragment) + TestCase.assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) + TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + } + + @Test + fun testGetFragment_assignment_offlineNotSynced() { + val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/123456789" + val moduleItem = ModuleItem( + id = 4567, + type = "Assignment", + url = url + ) + + val course = Course() + + val fragment = callGetFragment(moduleItem, course, null, isOnline = false) + TestCase.assertNotNull(fragment) + TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + } + @Test fun testGetFragment_assignmentShardId() { val url = "https://mobile.canvas.net/api/v1/courses/222/assignments/12345~6789" @@ -189,6 +296,24 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } + @Test + fun testGetFragment_externalTool_offline() { + val url = "https://instructure.com" + val moduleItem = ModuleItem( + id = 4567, + type = "ExternalUrl", + title = "Hello", + htmlUrl = url + + ) + + val course = Course() + + val fragment = callGetFragment(moduleItem, course, null, isOnline = false) + TestCase.assertNotNull(fragment) + TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + } + @Test fun testGetFragment_subheader() { val moduleItem = ModuleItem( @@ -210,7 +335,8 @@ class ModuleUtilityTest : TestCase() { id = 4567, type = "Quiz", url = url, - htmlUrl = htmlUrl + htmlUrl = htmlUrl, + contentId = 55 ) val course = Course() @@ -218,6 +344,7 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) expectedBundle.putString(Const.URL, htmlUrl) expectedBundle.putString(Const.API_URL, apiUrl) + expectedBundle.putLong(Const.ID, 55) val parentFragment = callGetFragment(moduleItem, course, null) TestCase.assertNotNull(parentFragment) @@ -225,6 +352,53 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } + @Test + fun testGetFragment_quiz_offlineSynced() { + val url = "https://mobile.canvas.net/api/v1/courses/222/quizzes/123456789" + val htmlUrl = "https://mobile.canvas.net/courses/222/quizzes/123456789" + val apiUrl = "courses/222/quizzes/123456789" + + val moduleItem = ModuleItem( + id = 4567, + type = "Quiz", + url = url, + htmlUrl = htmlUrl, + contentId = 55 + ) + + val course = Course() + val expectedBundle = Bundle() + expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) + expectedBundle.putString(Const.URL, htmlUrl) + expectedBundle.putString(Const.API_URL, apiUrl) + expectedBundle.putLong(Const.ID, 55) + + val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.QUIZZES_ID)) + TestCase.assertNotNull(parentFragment) + TestCase.assertEquals(ModuleQuizDecider::class.java, parentFragment!!.javaClass) + TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + } + + @Test + fun testGetFragment_quiz_offlineNotSynced() { + val url = "https://mobile.canvas.net/api/v1/courses/222/quizzes/123456789" + val htmlUrl = "https://mobile.canvas.net/courses/222/quizzes/123456789" + val apiUrl = "courses/222/quizzes/123456789" + + val moduleItem = ModuleItem( + id = 4567, + type = "Quiz", + url = url, + htmlUrl = htmlUrl, + contentId = 55 + ) + + val course = Course() + val fragment = callGetFragment(moduleItem, course, null, isOnline = false) + TestCase.assertNotNull(fragment) + TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + } + @Test fun testGetFragment_discussion() { val url = "https://mobile.canvas.net/api/v1/courses/222/discussion_topics/123456789" @@ -246,7 +420,22 @@ class ModuleUtilityTest : TestCase() { TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } - private fun callGetFragment(moduleItem: ModuleItem, course: Course, moduleObject: ModuleObject?): Fragment? { - return ModuleUtility.getFragment(moduleItem, course, moduleObject, false, false) + @Test + fun testGetFragment_discussion_offline() { + val url = "https://mobile.canvas.net/api/v1/courses/222/discussion_topics/123456789" + val moduleItem = ModuleItem( + id = 4567, + type = "Discussion", + url = url + ) + + val course = Course() + val fragment = callGetFragment(moduleItem, course, null, isOnline = false) + TestCase.assertNotNull(fragment) + TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + } + + private fun callGetFragment(moduleItem: ModuleItem, course: Course, moduleObject: ModuleObject?, isOnline: Boolean = true, tabs: Set = emptySet(), files: List = emptyList()): Fragment? { + return ModuleUtility.getFragment(moduleItem, course, moduleObject, false, false, isOnline, tabs, files, context) } } diff --git a/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt index 0f6c603e97..9fa5ce93f9 100644 --- a/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/util/RouterUtilsTest.kt @@ -17,6 +17,7 @@ package com.instructure.student.test.util +import androidx.fragment.app.FragmentActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.utils.ApiPrefs @@ -26,21 +27,32 @@ import com.instructure.interactions.router.RouterParams import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.student.activity.BaseRouterActivity -import com.instructure.student.features.assignmentdetails.AssignmentDetailsFragment +import com.instructure.student.features.assignments.details.AssignmentDetailsFragment +import com.instructure.student.features.assignments.list.AssignmentListFragment +import com.instructure.student.features.discussion.list.DiscussionListFragment +import com.instructure.student.features.grades.GradesListFragment +import com.instructure.student.features.modules.list.ModuleListFragment +import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment +import com.instructure.student.features.pages.list.PageListFragment +import com.instructure.student.features.people.details.PeopleDetailsFragment +import com.instructure.student.features.people.list.PeopleListFragment +import com.instructure.student.features.quiz.list.QuizListFragment import com.instructure.student.fragment.* import com.instructure.student.mobius.assignmentDetails.submissionDetails.ui.SubmissionDetailsFragment -import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListFragment -import com.instructure.student.mobius.syllabus.ui.SyllabusFragment +import com.instructure.student.mobius.conferences.conference_list.ui.ConferenceListRepositoryFragment +import com.instructure.student.mobius.syllabus.ui.SyllabusRepositoryFragment import com.instructure.student.router.RouteMatcher +import io.mockk.mockk import junit.framework.TestCase import org.junit.Test import org.junit.runner.RunWith -import org.robolectric.RuntimeEnvironment import java.util.* @RunWith(AndroidJUnit4::class) class RouterUtilsTest : TestCase() { + private val activity: FragmentActivity = mockk(relaxed = true) + @Test fun testCanRouteInternally_misc() { // Home @@ -78,7 +90,7 @@ class RouterUtilsTest : TestCase() { } private fun callCanRouteInternally(url: String): Boolean { - return RouteMatcher.canRouteInternally(RuntimeEnvironment.application, url, "mobiledev.instructure.com", false) + return RouteMatcher.canRouteInternally(activity, url, "mobiledev.instructure.com", false) } private fun callGetInternalRoute(url: String): Route? { @@ -354,7 +366,7 @@ class RouterUtilsTest : TestCase() { fun testGetInternalRoute_syllabus() { val route = callGetInternalRoute("https://mobiledev.instructure.com/courses/836357/assignments/syllabus") assertNotNull(route) - assertEquals(SyllabusFragment::class.java, route!!.primaryClass) + assertEquals(SyllabusRepositoryFragment::class.java, route!!.primaryClass) val expectedParams = HashMap() expectedParams[RouterParams.COURSE_ID] = "836357" @@ -452,13 +464,13 @@ class RouterUtilsTest : TestCase() { var route = callGetInternalRoute("https://mobiledev.instructure.com/courses/$courseId/conferences/") val expectedParams = hashMapOf(RouterParams.COURSE_ID to courseId) assertNotNull(route) - assertEquals(ConferenceListFragment::class.java, route!!.primaryClass) + assertEquals(ConferenceListRepositoryFragment::class.java, route!!.primaryClass) assertEquals(expectedParams, route.paramsHash) // There is currently no API endpoint for specific conferences, so we must route to the conference list route = callGetInternalRoute("https://mobiledev.instructure.com/courses/$courseId/conferences/234") // not an actual url assertNotNull(route) - assertEquals(ConferenceListFragment::class.java, route!!.primaryClass) + assertEquals(ConferenceListRepositoryFragment::class.java, route!!.primaryClass) assertEquals(expectedParams, route.paramsHash) } diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 0916c4c363..3ffda0139f 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -39,8 +39,8 @@ android { defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 59 - versionName = '1.25.0' + versionCode = 60 + versionName = '1.26.0' vectorDrawables.useSupportLibrary = true multiDexEnabled true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' diff --git a/apps/teacher/flank_e2e_lowres.yml b/apps/teacher/flank_e2e_lowres.yml index 8e6947ac6d..b6e24c1447 100644 --- a/apps/teacher/flank_e2e_lowres.yml +++ b/apps/teacher/flank_e2e_lowres.yml @@ -16,7 +16,7 @@ gcloud: - notAnnotation com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - model: NexusLowRes - version: 26 + version: 29 locale: en_US orientation: portrait diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt index 31830d0aea..51b297cb5d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt @@ -83,6 +83,7 @@ class InboxPageTest: TeacherTest() { @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) fun archiveMultipleConversations() { val data = createInitialData() + val isTablet = isTabletDevice() val conversation1 = data.addConversation( senderId = data.students.first().id, @@ -103,6 +104,7 @@ class InboxPageTest: TeacherTest() { inboxPage.assertConversationNotDisplayed(conversation2.subject!!) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) + if(!isTablet) inboxPage.refresh() inboxPage.filterMessageScope("Archived") inboxPage.assertConversationDisplayed(conversation1.subject!!) inboxPage.assertConversationDisplayed(conversation2.subject!!) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt index fb91c72a4a..30257cff90 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AnnouncementsE2ETest.kt @@ -41,6 +41,9 @@ class AnnouncementsE2ETest : TeacherTest() { override fun enableAndConfigureAccessibilityChecks() = Unit + //Because of naming conventions, we are using 'announcementDetailsPage' naming in this class to make the code more readable and straightforward. + private val announcementDetailsPage = discussionsDetailsPage + /** * Test announcements e2e * @@ -82,10 +85,10 @@ class AnnouncementsE2ETest : TeacherTest() { announcementsListPage.assertSearchResultCount(2) Log.d(STEP_TAG,"Edit ${announcement.title} announcement's name to 'Haha'. Save the modifications.") - announcementsListPage.clickDiscussion(announcement) - editAnnouncementPage.openEdit() - editAnnouncementPage.editAnnouncementName("Haha") - editAnnouncementPage.saveEditAnnouncement() + announcementsListPage.clickAnnouncement(announcement) + announcementDetailsPage.openEdit() + editAnnouncementDetailsPage.editAnnouncementTitle("Haha") + editAnnouncementDetailsPage.saveAnnouncement() Log.d(STEP_TAG,"Navigate back to the Announcements Page. Refresh the page and assert that the announcement name has been changed to 'Haha'.") Espresso.pressBack() @@ -93,14 +96,14 @@ class AnnouncementsE2ETest : TeacherTest() { announcementsListPage.assertHasAnnouncement("Haha") Log.d(STEP_TAG,"Delete the 'Haha' titled announcement.") - announcementsListPage.clickDiscussion("Haha") - editAnnouncementPage.openEdit() - editAnnouncementPage.deleteAnnouncement() + announcementsListPage.clickAnnouncement("Haha") + announcementDetailsPage.openEdit() + editAnnouncementDetailsPage.deleteAnnouncement() Log.d(STEP_TAG, "") - announcementsListPage.clickDiscussion(announcement2.title) - editAnnouncementPage.openEdit() - editAnnouncementPage.deleteAnnouncement() + announcementsListPage.clickAnnouncement(announcement2.title) + announcementDetailsPage.openEdit() + editAnnouncementDetailsPage.deleteAnnouncement() Log.d(STEP_TAG,"Refresh the Announcements Page and assert that there is no announcement displayed. Assert that empty view is displayed.") announcementsListPage.refresh() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt index 82593033cd..95ff85feea 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/AssignmentE2ETest.kt @@ -196,6 +196,19 @@ class AssignmentE2ETest : TeacherTest() { assignmentListPage.assertHasAssignment(assignment[0]) assignmentListPage.assertNeedsGradingCountOfAssignment(assignment[0].name, 1) + Log.d(STEP_TAG,"Click on Search button and type '${quizAssignment[0].name}' to the search input field.") + assignmentListPage.searchable.clickOnSearchButton() + assignmentListPage.searchable.typeToSearchBar(quizAssignment[0].name) + + Log.d(STEP_TAG, "Assert that the '${quizAssignment[0].name}' quiz assignment is the only one which is displayed because it matches the search text.") + assignmentListPage.assertHasAssignment(quizAssignment[0]) + assignmentListPage.assertAssignmentNotDisplayed(assignment[0]) + + Log.d(STEP_TAG,"Clear search input field value and assert if both of the assignment are displayed again on the Assignment List Page.") + assignmentListPage.searchable.clickOnClearSearchButton() + assignmentListPage.assertHasAssignment(assignment[0]) + assignmentListPage.assertHasAssignment(quizAssignment[0]) + val newAssignmentName = "New Assignment Name" Log.d(STEP_TAG,"Edit ${assignment[0].name} assignment's name to: $newAssignmentName.") assignmentListPage.clickAssignment(assignment[0]) @@ -336,7 +349,6 @@ class AssignmentE2ETest : TeacherTest() { Espresso.pressBack() speedGraderCommentsPage.clickOnVideoComment() speedGraderCommentsPage.assertMediaCommentPreviewDisplayed() - } @E2E diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt index d1e7633b04..21dd220547 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DashboardE2ETest.kt @@ -69,7 +69,7 @@ class DashboardE2ETest : TeacherTest() { dashboardPage.assertDisplaysCourse(course1) dashboardPage.assertDisplaysCourse(course2) - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button. Assert that the Edit Dashboard Page is loaded.") + Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() @@ -90,7 +90,7 @@ class DashboardE2ETest : TeacherTest() { dashboardPage.assertOpensCourse(course2) Espresso.pressBack() - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button. Assert that the Edit Dashboard Page is loaded.") + Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() @@ -106,7 +106,7 @@ class DashboardE2ETest : TeacherTest() { dashboardPage.assertDisplaysCourse(course1) dashboardPage.assertDisplaysCourse(course2) - Log.d(STEP_TAG,"Click on 'Edit Dashboard' button. Assert that the Edit Dashboard Page is loaded.") + Log.d(STEP_TAG,"Click on 'All Courses' button. Assert that the All Courses Page is loaded.") dashboardPage.clickEditDashboard() editDashboardPage.assertPageObjects() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt index 4d80738c64..7fed199a13 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/DiscussionsE2ETest.kt @@ -66,8 +66,8 @@ class DiscussionsE2ETest : TeacherTest() { val newTitle = "New Discussion" Log.d(STEP_TAG,"Edit the discussion's title to: '$newTitle'. Click on 'Save'.") - editDiscussionsDetailsPage.editTitle(newTitle) - editDiscussionsDetailsPage.clickSave() + editDiscussionsDetailsPage.editDiscussionTitle(newTitle) + editDiscussionsDetailsPage.saveDiscussion() Log.d(STEP_TAG,"Refresh the page. Assert that the discussion's name has been changed to '$newTitle' and it is published.") discussionsDetailsPage.refresh() @@ -77,7 +77,7 @@ class DiscussionsE2ETest : TeacherTest() { Log.d(STEP_TAG,"Navigate to Discussions Details Page by clicking on 'Edit'. Unpublish the '$newTitle' discussion and click on 'Save'.") discussionsDetailsPage.openEdit() editDiscussionsDetailsPage.togglePublished() - editDiscussionsDetailsPage.clickSave() + editDiscussionsDetailsPage.saveDiscussion() Log.d(STEP_TAG,"Refresh the page. Assert that the '$newTitle' discussion has been unpublished.") discussionsDetailsPage.refresh() @@ -111,8 +111,8 @@ class DiscussionsE2ETest : TeacherTest() { val newDiscussionTitle = "Test Discussion Mobile UI" Log.d(STEP_TAG,"Set '$newDiscussionTitle' as the discussion's title and set some description as well.") - editDiscussionsDetailsPage.editTitle(newDiscussionTitle) - editDiscussionsDetailsPage.editDescription("Mobile UI Discussion description") + editDiscussionsDetailsPage.editDiscussionTitle(newDiscussionTitle) + editDiscussionsDetailsPage.editDiscussionDescription("Mobile UI Discussion description") Log.d(STEP_TAG,"Toggle Publish checkbox and save the page.") editDiscussionsDetailsPage.togglePublished() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt index 72bd37f362..f9ecf29cbc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt @@ -24,11 +24,11 @@ class InboxE2ETest : TeacherTest() { override fun enableAndConfigureAccessibilityChecks() = Unit + @E2E @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) - fun testInboxE2E() { - + fun testInboxMessageComposeReplyAndOptionMenuActionsE2E() { Log.d(PREPARATION_TAG, "Seeding data.") val data = seedData(students = 2, teachers = 1, courses = 1) val teacher = data.teachersList[0] @@ -152,6 +152,43 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Assert that the '${seedConversation[0]}' conversation is disappeared because it's not starred yet.") dashboardPage.assertPageObjects() inboxPage.assertInboxEmpty() + } + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) + fun testInboxSelectedButtonActionsE2E() { + + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 2, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student1 = data.studentsList[0] + val student2 = data.studentsList[1] + + val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) + val group = GroupsApi.createGroup(groupCategory.id, teacher.token) + Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} student to the group: ${group.name}.") + GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) + + Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.") + tokenLogin(teacher) + dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course) + + Log.d(STEP_TAG,"Open Inbox. Assert that Inbox is empty.") + dashboardPage.openInbox() + inboxPage.assertInboxEmpty() + + Log.d(PREPARATION_TAG, "Seed an Inbox conversation via API.") + val seedConversation = ConversationsApi.createConversation( + token = student1.token, + recipients = listOf(teacher.id.toString()) + ) + + Log.d(STEP_TAG, "Refresh the page. Assert that the conversation displayed as unread.") + inboxPage.refresh() + inboxPage.assertThereIsAnUnreadMessage(true) Log.d(PREPARATION_TAG, "Seed another Inbox conversation via API.") val seedConversation2 = ConversationsApi.createConversation( @@ -169,7 +206,8 @@ class InboxE2ETest : TeacherTest() { body = "Third body" ) - Log.d(STEP_TAG,"Filter the Inbox by selecting 'Inbox' category from the spinner on Inbox Page. Assert that the '${seedConversation[0]}' conversation is displayed.") + Log.d(STEP_TAG,"Refresh the page. Filter the Inbox by selecting 'Inbox' category from the spinner on Inbox Page. Assert that the '${seedConversation[0]}' conversation is displayed. Assert that the conversation is unread yet.") + inboxPage.refresh() inboxPage.filterMessageScope("Inbox") inboxPage.assertHasConversation() @@ -185,8 +223,8 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and unarchive it." + "Assert that the selected number of conversation on the toolbar is 1 and '${seedConversation2[0].subject}' conversation is not displayed in the 'ARCHIVED' scope.") inboxPage.selectConversation(seedConversation2[0]) - inboxPage.clickUnArchive() inboxPage.assertSelectedConversationNumber("1") + inboxPage.clickUnArchive() inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that ${seedConversation2[0].subject} conversation is displayed.") @@ -196,8 +234,8 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select both of the conversations (${seedConversation[0].subject} and ${seedConversation2[0].subject} and star them." + "Assert that both of the has been starred and the selected number of conversations on the toolbar shows 2") inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject)) - inboxPage.clickStar() inboxPage.assertSelectedConversationNumber("2") + inboxPage.clickStar() inboxPage.assertConversationStarred(seedConversation2[0].subject) inboxPage.assertConversationStarred(seedConversation3[0].subject) @@ -252,15 +290,69 @@ class InboxE2ETest : TeacherTest() { inboxPage.filterMessageScope("Inbox") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationDisplayed(seedConversation3[0].subject) + } + + @E2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.INBOX, TestCategory.E2E) + fun testInboxSwipeGesturesE2E() { + Log.d(PREPARATION_TAG, "Seeding data.") + val data = seedData(students = 2, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student1 = data.studentsList[0] + val student2 = data.studentsList[1] + + val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) + val group = GroupsApi.createGroup(groupCategory.id, teacher.token) + Log.d(PREPARATION_TAG, "Create group membership for ${student1.name} student to the group: ${group.name}.") + GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) + + Log.d(STEP_TAG, "Login with user: ${teacher.name}, login id: ${teacher.loginId}.") + tokenLogin(teacher) + dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course) + + Log.d(STEP_TAG,"Open Inbox. Assert that Inbox is empty.") + dashboardPage.openInbox() + inboxPage.assertInboxEmpty() + + Log.d(PREPARATION_TAG, "Seed an Inbox conversation via API.") + val seedConversation = ConversationsApi.createConversation( + token = student1.token, + recipients = listOf(teacher.id.toString()) + ) + + Log.d(STEP_TAG,"Refresh the page. Assert that the previously seeded Inbox conversation is displayed. Assert that the message is unread yet.") + inboxPage.refresh() + inboxPage.assertHasConversation() + inboxPage.assertThereIsAnUnreadMessage(true) - Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and swipe it right to make it unread. Assert that the conversation became unread.") + Log.d(PREPARATION_TAG, "Seed another Inbox conversation via API.") + val seedConversation2 = ConversationsApi.createConversation( + token = student1.token, + recipients = listOf(teacher.id.toString()), + subject = "Second conversation", + body = "Second body" + ) + + Log.d(PREPARATION_TAG, "Seed a third Inbox conversation via API.") + val seedConversation3 = ConversationsApi.createConversation( + token = student2.token, + recipients = listOf(teacher.id.toString()), + subject = "Third conversation", + body = "Third body" + ) + + Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and swipe it right to make it read. Assert that the conversation became read.") + inboxPage.refresh() inboxPage.selectConversation(seedConversation2[0].subject) inboxPage.swipeConversationRight(seedConversation2[0]) - inboxPage.assertUnreadMarkerVisibility(seedConversation2[0].subject, ViewMatchers.Visibility.VISIBLE) + inboxPage.assertUnreadMarkerVisibility(seedConversation2[0].subject, ViewMatchers.Visibility.GONE) - Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and swipe it right again to make it read. Assert that the conversation became read.") + Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and swipe it right again to make it unread. Assert that the conversation became unread.") inboxPage.swipeConversationRight(seedConversation2[0]) - inboxPage.assertUnreadMarkerVisibility(seedConversation2[0].subject, ViewMatchers.Visibility.GONE) + inboxPage.assertUnreadMarkerVisibility(seedConversation2[0].subject, ViewMatchers.Visibility.VISIBLE) Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'INBOX' scope because it has became archived.") inboxPage.swipeConversationLeft(seedConversation2[0]) @@ -281,7 +373,7 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject)) inboxPage.clickStar() - inboxPage.clickMarkAsUnread() + inboxPage.clickMarkAsRead() Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") inboxPage.filterMessageScope("Starred") @@ -292,42 +384,42 @@ class InboxE2ETest : TeacherTest() { inboxPage.swipeConversationLeft(seedConversation2[0]) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) - Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is unread.") - inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.VISIBLE) + Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.") + inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) - Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation right and assert that it has became read.") + Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation right and assert that it has became unread.") inboxPage.swipeConversationRight(seedConversation3[0].subject) - inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) + inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.VISIBLE) - Log.d(STEP_TAG, "Navigate to 'UNREAD' scope. Assert that only the '${seedConversation2[0].subject}' conversation is displayed in the 'UNREAD' scope.") + Log.d(STEP_TAG, "Navigate to 'UNREAD' scope. Assert that only the '${seedConversation3[0].subject}' conversation is displayed in the 'UNREAD' scope.") inboxPage.filterMessageScope("Unread") - inboxPage.assertConversationDisplayed(seedConversation2[0].subject) - inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) - - Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' conversation left and assert it has been removed from the 'UNREAD' scope since it has became read.") - inboxPage.swipeConversationLeft(seedConversation2[0]) + inboxPage.assertConversationDisplayed(seedConversation3[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) + Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation left and assert it has been removed from the 'UNREAD' scope since it has became read.") + inboxPage.swipeConversationLeft(seedConversation3[0]) + inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) + Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") inboxPage.filterMessageScope("Archived") - inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + inboxPage.assertConversationDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'INBOX' scope and select '${seedConversation3[0].subject}' conversation.") inboxPage.filterMessageScope("Inbox") - inboxPage.selectConversation(seedConversation3[0].subject) + inboxPage.selectConversation(seedConversation2[0].subject) - Log.d(STEP_TAG, "Delete the '${seedConversation3[0].subject}' conversation and assert that it has been removed from the 'INBOX' scope.") + Log.d(STEP_TAG, "Delete the '${seedConversation2[0].subject}' conversation and assert that it has been removed from the 'INBOX' scope.") inboxPage.clickDelete() inboxPage.confirmDelete() - inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) + inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) - Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") + Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seedConversation3[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") inboxPage.filterMessageScope("Archived") - Log.d(STEP_TAG,"Click on the '${seedConversation2[0].subject}' conversation.") - inboxPage.clickConversation(seedConversation2[0]) + Log.d(STEP_TAG,"Click on the '${seedConversation3[0].subject}' conversation.") + inboxPage.clickConversation(seedConversation3[0]) - Log.d(STEP_TAG, "Delete the '${seedConversation2[0]}' conversation and assert that it has disappeared from the list.") + Log.d(STEP_TAG, "Delete the '${seedConversation3[0]}' conversation and assert that it has disappeared from the list.") inboxMessagePage.deleteConversation() Log.d(STEP_TAG, "Assert that the empty view is displayed.") diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt index 9810fa4d0c..7f9117fd15 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/ModulesE2ETest.kt @@ -1,15 +1,20 @@ package com.instructure.teacher.ui.e2e import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.E2E import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.api.DiscussionTopicsApi import com.instructure.dataseeding.api.ModulesApi +import com.instructure.dataseeding.api.PagesApi import com.instructure.dataseeding.api.QuizzesApi import com.instructure.dataseeding.model.AssignmentApiModel import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.ModuleApiModel import com.instructure.dataseeding.model.ModuleItemTypes +import com.instructure.dataseeding.model.PageApiModel import com.instructure.dataseeding.model.QuizApiModel import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days @@ -19,6 +24,7 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData +import com.instructure.teacher.ui.pages.WebViewTextCheck import com.instructure.teacher.ui.utils.TeacherTest import com.instructure.teacher.ui.utils.seedData import com.instructure.teacher.ui.utils.tokenLogin @@ -59,19 +65,34 @@ class ModulesE2ETest : TeacherTest() { Log.d(PREPARATION_TAG,"Seeding quiz for ${course.name} course.") val quiz = createQuiz(course, teacher) + Log.d(PREPARATION_TAG,"Create an unpublished page for course: ${course.name}.") + val testPage = createCoursePage(course, teacher, published = false, frontPage = false, body = "

Test Page Text

") + + Log.d(PREPARATION_TAG,"Create a discussion topic for ${course.name} course.") + val discussionTopic = createDiscussion(course, teacher) + Log.d(PREPARATION_TAG,"Seeding a module for ${course.name} course. It starts as unpublished.") val module = createModule(course, teacher) Log.d(PREPARATION_TAG,"Associate ${assignment.name} assignment (and the quiz within it) with module: ${module.id}.") - createModuleAssignmentItem(course, module, teacher, assignment) + createModuleItem(course, module, teacher, assignment.name, ModuleItemTypes.ASSIGNMENT.stringVal, assignment.id.toString()) + createModuleItem(course, module, teacher, quiz.title, ModuleItemTypes.QUIZ.stringVal, quiz.id.toString()) - createModuleQuizItem(course, module, teacher, quiz) + Log.d(PREPARATION_TAG,"Associate ${testPage.title} page with module: ${module.id}.") + createModuleItem(course, module, teacher, testPage.title, ModuleItemTypes.PAGE.stringVal, null, pageUrl = testPage.url) + + Log.d(PREPARATION_TAG,"Associate ${discussionTopic.title} discussion with module: ${module.id}.") + createModuleItem(course, module, teacher, discussionTopic.title, ModuleItemTypes.DISCUSSION.stringVal, discussionTopic.id.toString()) Log.d(STEP_TAG,"Refresh the page. Assert that ${module.name} module is displayed and it is unpublished by default.") modulesPage.refresh() modulesPage.assertModuleIsDisplayed(module.name) modulesPage.assertModuleNotPublished() + Log.d(STEP_TAG,"Assert that ${testPage.title} page is present as a module item, but it's not published.") + modulesPage.assertModuleItemIsDisplayed(testPage.title) + modulesPage.assertModuleItemNotPublished(module.name, testPage.title) + Log.d(PREPARATION_TAG,"Publish ${module.name} module via API.") ModulesApi.updateModule( courseId = course.id, @@ -91,42 +112,74 @@ class ModulesE2ETest : TeacherTest() { modulesPage.assertModuleItemIsDisplayed(quiz.title) modulesPage.assertModuleItemIsPublished(quiz.title) + Log.d(STEP_TAG,"Assert that ${testPage.title} page is present as a module item, but it's not published.") + modulesPage.assertModuleItemIsDisplayed(testPage.title) + modulesPage.assertModuleItemIsPublished(testPage.title) + + Log.d(STEP_TAG, "Collapse the ${module.name} and assert that the module items has not displayed.") modulesPage.clickOnCollapseExpandIcon() modulesPage.assertItemCountInModule(module.name, 0) + Log.d(STEP_TAG, "Expand the ${module.name} and assert that the module items are displayed.") modulesPage.clickOnCollapseExpandIcon() - modulesPage.assertItemCountInModule(module.name, 2) - } + modulesPage.assertItemCountInModule(module.name, 4) - private fun createModuleQuizItem( - course: CourseApiModel, - module: ModuleApiModel, - teacher: CanvasUserApiModel, - quiz: QuizApiModel - ) { - ModulesApi.createModuleItem( + Log.d(PREPARATION_TAG,"Unpublish ${module.name} module via API.") + ModulesApi.updateModule( courseId = course.id, - moduleId = module.id, - teacherToken = teacher.token, - title = quiz.title, - type = ModuleItemTypes.QUIZ.stringVal, - contentId = quiz.id.toString() + id = module.id, + published = false, + teacherToken = teacher.token ) + + Log.d(STEP_TAG, "Refresh the Modules Page.") + modulesPage.refresh() + + Log.d(STEP_TAG,"Assert that ${assignment.name} assignment and ${quiz.title} quiz and ${testPage.title} page are present as module items, and they are NOT published since their module is unpublished.") + modulesPage.assertModuleItemIsDisplayed(assignment.name) + modulesPage.assertModuleItemNotPublished(module.name, assignment.name) + modulesPage.assertModuleItemIsDisplayed(quiz.title) + modulesPage.assertModuleItemNotPublished(module.name, quiz.title) + modulesPage.assertModuleItemIsDisplayed(testPage.title) + modulesPage.assertModuleItemNotPublished(module.name, testPage.title) + + Log.d(STEP_TAG, "Open the ${assignment.name} assignment module item and assert that the Assignment Details Page is displayed. Navigate back to Modules Page.") + modulesPage.clickOnModuleItem(assignment.name) + assignmentDetailsPage.assertPageObjects() + Espresso.pressBack() + + Log.d(STEP_TAG, "Open the ${quiz.title} quiz module item and assert that the Quiz Details Page is displayed. Navigate back to Modules Page.") + modulesPage.clickOnModuleItem(quiz.title) + quizDetailsPage.assertPageObjects() + Espresso.pressBack() + + Log.d(STEP_TAG, "Open the ${testPage.title} page module item and assert that the Page Details Page is displayed. Navigate back to Modules Page.") + modulesPage.clickOnModuleItem(testPage.title) + editPageDetailsPage.runTextChecks(WebViewTextCheck(Locator.ID, "header1", "Test Page Text")) + Espresso.pressBack() + + Log.d(STEP_TAG, "Open the ${discussionTopic.title} discussion module item and assert that the Discussion Details Page is displayed.") + modulesPage.clickOnModuleItem(discussionTopic.title) + discussionsDetailsPage.assertPageObjects() } - private fun createModuleAssignmentItem( + private fun createModuleItem( course: CourseApiModel, module: ModuleApiModel, teacher: CanvasUserApiModel, - assignment: AssignmentApiModel + title: String, + moduleItemType: String, + contentId: String?, + pageUrl: String? = null ) { ModulesApi.createModuleItem( courseId = course.id, moduleId = module.id, teacherToken = teacher.token, - title = assignment.name, - type = ModuleItemTypes.ASSIGNMENT.stringVal, - contentId = assignment.id.toString() + title = title, + type = moduleItemType, + contentId = contentId, + pageUrl = pageUrl ) } @@ -171,4 +224,28 @@ class ModulesE2ETest : TeacherTest() { ) } + private fun createCoursePage( + course: CourseApiModel, + teacher: CanvasUserApiModel, + published: Boolean = true, + frontPage: Boolean = false, + body: String = EMPTY_STRING + ): PageApiModel { + return PagesApi.createCoursePage( + courseId = course.id, + published = published, + frontPage = frontPage, + token = teacher.token, + body = body + ) + } + + private fun createDiscussion( + course: CourseApiModel, + teacher: CanvasUserApiModel + ) = DiscussionTopicsApi.createDiscussion( + courseId = course.id, + token = teacher.token + ) + } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt index 88b00f66ec..932b13e31f 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AnnouncementsListPage.kt @@ -42,9 +42,9 @@ import com.instructure.teacher.R import com.instructure.teacher.ui.utils.TypeInRCETextEditor /** - * Announcements list page + * Announcements list page. * - * @constructor Create empty Announcements list page + * @constructor Create empty Announcements list page. */ class AnnouncementsListPage(val searchable: Searchable) : BasePage() { @@ -56,19 +56,19 @@ class AnnouncementsListPage(val searchable: Searchable) : BasePage() { /** * Click on the discussion given in parameter. * - * @param discussion: The DiscussionApiModel parameter. + * @param announcement: The DiscussionApiModel parameter. */ - fun clickDiscussion(discussion: DiscussionApiModel) { - clickDiscussion(discussion.title) + fun clickAnnouncement(announcement: DiscussionApiModel) { + clickAnnouncement(announcement.title) } /** * Click on the discussion with the given title in parameter. * - * @param discussionTitle: The discussion title parameter string. + * @param announcementTitle: The discussion title parameter string. */ - fun clickDiscussion(discussionTitle: String) { - waitForViewWithText(discussionTitle).click() + fun clickAnnouncement(announcementTitle: String) { + waitForViewWithText(announcementTitle).click() } /** diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentListPage.kt index 99943b489e..0bf5665ac8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AssignmentListPage.kt @@ -22,15 +22,19 @@ import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import com.instructure.canvasapi2.models.Assignment import com.instructure.dataseeding.model.AssignmentApiModel +import com.instructure.espresso.DoesNotExistAssertion import com.instructure.espresso.OnViewWithId import com.instructure.espresso.RecyclerViewItemCountAssertion +import com.instructure.espresso.Searchable import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.waitForViewWithText +import com.instructure.espresso.page.withAncestor import com.instructure.espresso.page.withId import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText @@ -45,7 +49,7 @@ import org.hamcrest.CoreMatchers.allOf * * @constructor Creates an instance of the AssignmentListPage. */ -class AssignmentListPage : BasePage() { +class AssignmentListPage(val searchable: Searchable) : BasePage() { private val assignmentListToolbar by OnViewWithId(R.id.assignmentListToolbar) private val assignmentRecyclerView by OnViewWithId(R.id.assignmentRecyclerView) @@ -97,6 +101,15 @@ class AssignmentListPage : BasePage() { assertAssignmentName(assignment.name) } + /** + * Asserts that the given assignment is NOT present in the list. + * + * @param assignment The assignment to check. + */ + fun assertAssignmentNotDisplayed(assignment: AssignmentApiModel) { + onView(withText(assignment.name)).check(DoesNotExistAssertion(10)) + } + /** * Asserts that grading periods are present. */ @@ -137,7 +150,7 @@ class AssignmentListPage : BasePage() { } private fun assertAssignmentName(assignmentName: String) { - waitForViewWithText(assignmentName).assertDisplayed() + waitForView(withText(assignmentName) + withAncestor(R.id.assignmentLayout)).assertDisplayed() } /** diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt index 34bdf1ef51..cd75a2eb89 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt @@ -52,7 +52,7 @@ import org.hamcrest.Matcher * * This class extends the `BasePage` class and provides methods for interacting with the dashboard, * such as asserting the display of courses, empty view, and course title; opening and switching courses; - * clicking on the edit dashboard button and course overflow menu; changing the course nickname; + * clicking on the All Courses button and course overflow menu; changing the course nickname; * asserting the display of notifications; and opening the inbox and todo tabs. * * @constructor Creates an instance of the `DashboardPage` class. @@ -155,7 +155,7 @@ class DashboardPage : BasePage() { } /** - * Asserts that the dashboard displays the courses, including the toolbar, courses view, and edit dashboard button. + * Asserts that the dashboard displays the courses, including the toolbar, courses view, and All Courses button. */ fun assertDisplaysCourses() { emptyView.assertNotDisplayed() @@ -176,7 +176,7 @@ class DashboardPage : BasePage() { } /** - * Clicks on the edit dashboard button. + * Clicks on the All Courses button. */ fun clickEditDashboard() { onView(withId(R.id.editDashboardTextView)).click() @@ -279,7 +279,7 @@ class DashboardPage : BasePage() { */ fun changeCourseNickname(changeTo: String) { onView(withId(R.id.newCourseNickname)).replaceText(changeTo) - onView(withText(R.string.ok) + withAncestor(R.id.buttonPanel)).click() + onView(withText(android.R.string.ok) + withAncestor(R.id.buttonPanel)).click() } /** diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt index f6f8b9848c..39cd5a6575 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DiscussionsListPage.kt @@ -160,7 +160,7 @@ class DiscussionsListPage(val searchable: Searchable) : BasePage() { */ fun selectOverFlowMenu(menuText: String) { waitForView(withText(menuText) + withParent(R.id.coursePages)).click() - onView(withText(R.string.ok) + withAncestor(R.id.buttonPanel)).click() + onView(withText(android.R.string.ok) + withAncestor(R.id.buttonPanel)).click() } /** diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt similarity index 82% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementPage.kt rename to apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt index 2bb5119eda..9415744a68 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditAnnouncementDetailsPage.kt @@ -20,37 +20,29 @@ import androidx.test.espresso.Espresso import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView -import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.withId import com.instructure.espresso.replaceText import com.instructure.espresso.scrollTo import com.instructure.teacher.R /** - * Represents the Edit Announcement page. + * Represents the Edit Announcement Page. */ -class EditAnnouncementPage : BasePage() { - - /** - * Opens the edit mode for the announcement. - */ - fun openEdit() { - waitForView(withId(R.id.menu_edit)).click() - } +class EditAnnouncementDetailsPage : BasePage() { /** * Edits the name of the announcement with the specified [newName]. * * @param newName The new name for the announcement. */ - fun editAnnouncementName(newName: String) { + fun editAnnouncementTitle(newName: String) { onView(withId(R.id.announcementNameEditText)).replaceText(newName) } /** * Saves the edited announcement. */ - fun saveEditAnnouncement() { + fun saveAnnouncement() { onView(withId(R.id.menuSaveAnnouncement)).click() } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt index ba26777b0f..8f76e47073 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDashboardPage.kt @@ -27,7 +27,7 @@ import com.instructure.espresso.page.* import com.instructure.teacher.R /** - * A page representing the Edit Dashboard screen in the application. + * A page representing the All Courses screen in the application. */ @Suppress("unused") class EditDashboardPage : BasePage() { @@ -43,7 +43,7 @@ class EditDashboardPage : BasePage() { private val allCoursesLabel by WaitForViewWithText(R.string.allCourses) /** - * Asserts that the Edit Dashboard screen displays the given list of courses. + * Asserts that the All Courses screen displays the given list of courses. * * @param mCourses The list of courses to verify. */ @@ -55,7 +55,7 @@ class EditDashboardPage : BasePage() { } /** - * Asserts that the Edit Dashboard screen displays a specific course. + * Asserts that the All Courses screen displays a specific course. * * @param courseName The name of the course to verify. */ @@ -64,7 +64,7 @@ class EditDashboardPage : BasePage() { } /** - * Asserts that a specific course is favored in the Edit Dashboard screen. + * Asserts that a specific course is favored in the All Courses screen. * * @param course The course to verify. */ @@ -75,7 +75,7 @@ class EditDashboardPage : BasePage() { } /** - * Asserts that a specific course is not favored in the Edit Dashboard screen. + * Asserts that a specific course is not favored in the All Courses screen. * * @param course The course to verify. */ @@ -86,7 +86,7 @@ class EditDashboardPage : BasePage() { } /** - * Toggles favoring/unfavoring a specific course in the Edit Dashboard screen. + * Toggles favoring/unfavoring a specific course in the All Courses screen. * * @param courseName The name of the course to toggle favoring. */ @@ -109,7 +109,7 @@ class EditDashboardPage : BasePage() { } /** - * Clicks on the mass select button in the Edit Dashboard screen based on the selection state. + * Clicks on the mass select button in the All Courses screen based on the selection state. * * @param someSelected Indicates whether some items are selected or not. */ diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt index fa67b64638..e89ad6cc21 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/EditDiscussionsDetailsPage.kt @@ -42,7 +42,7 @@ class EditDiscussionsDetailsPage : BasePage() { * * @param newTitle The new title of the discussion. */ - fun editTitle(newTitle: String) { + fun editDiscussionTitle(newTitle: String) { onView(withId(R.id.editDiscussionName)).replaceText(newTitle) Espresso.closeSoftKeyboard() } @@ -65,7 +65,7 @@ class EditDiscussionsDetailsPage : BasePage() { /** * Clicks the save button. This method is used when editing an existing discussion. */ - fun clickSave() { + fun saveDiscussion() { onView(withId(R.id.menuSave)).click() } @@ -81,7 +81,7 @@ class EditDiscussionsDetailsPage : BasePage() { * * @param newDescription The new description of the discussion. */ - fun editDescription(newDescription: String) { + fun editDiscussionDescription(newDescription: String) { contentRceView.perform(TypeInRCETextEditor(newDescription)) } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt index 52634c0ee2..2605294f93 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/FileListPage.kt @@ -153,7 +153,7 @@ class FileListPage(val searchable: Searchable) : BasePage(R.id.fileListPage) { waitForView(withId(R.id.addFolderFab)).click() waitForView(withId(R.id.alertTitle)).assertDisplayed() onView(withId(R.id.newFolderName)).typeText(folderName) - onView(withText(R.string.ok)).click() + onView(withText(android.R.string.ok)).click() } /** diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt index f0f8b3d0f5..55acb833fc 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ModulesPage.kt @@ -75,6 +75,15 @@ class ModulesPage : BasePage() { onView(allOf(withId(R.id.moduleItemTitle), withText(itemTitle))).assertDisplayed() } + /** + * Click on the module item with the given title to open it's detailer. + * + * @param itemTitle The title of the module item. + */ + fun clickOnModuleItem(itemTitle: String) { + onView(allOf(withId(R.id.moduleItemTitle), withText(itemTitle))).click() + } + /** * Asserts that the module item with the specified name is published. * diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderGradePage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderGradePage.kt index fb024f7477..2547b16f03 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderGradePage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SpeedGraderGradePage.kt @@ -21,7 +21,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isEnabled import com.instructure.espresso.OnViewWithId import com.instructure.espresso.OnViewWithText import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.WaitForViewWithStringText import com.instructure.espresso.WaitForViewWithText import com.instructure.espresso.assertContainsText import com.instructure.espresso.assertDisplayed @@ -35,12 +34,12 @@ import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithId import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText import com.instructure.espresso.replaceText import com.instructure.teacher.R import org.hamcrest.Matchers import org.hamcrest.Matchers.not import java.text.DecimalFormat -import java.util.* /** * Represents the SpeedGrader grade page. @@ -66,7 +65,6 @@ class SpeedGraderGradePage : BasePage() { //dialog views private val gradeEditText by WaitForViewWithId(R.id.gradeEditText) private val customizeGradeTitle by WaitForViewWithText(R.string.customize_grade) - private val confirmDialogButton by WaitForViewWithStringText(getStringFromResource(android.R.string.ok).uppercase(Locale.getDefault())) /** @@ -83,7 +81,7 @@ class SpeedGraderGradePage : BasePage() { */ fun enterNewGrade(grade: String) { gradeEditText.replaceText(grade) - confirmDialogButton.click() + onView(withText(android.R.string.ok)).click() } /** diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt index adda2fc21b..df93e38cf4 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/SyllabusPage.kt @@ -17,6 +17,7 @@ package com.instructure.teacher.ui.pages import android.app.Activity +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.web.assertion.WebViewAssertions import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms.findElement @@ -44,7 +45,6 @@ import org.hamcrest.Matchers.comparesEqualTo open class SyllabusPage : BasePage(R.id.syllabusPage) { private val tabs by OnViewWithId(R.id.syllabusTabLayout) - private val webView by WaitForViewWithId(R.id.contentWebView) /** * Asserts the display of an item on the Syllabus page. @@ -107,7 +107,7 @@ open class SyllabusPage : BasePage(R.id.syllabusPage) { */ fun assertDisplaysSyllabus(syllabusBody: String, shouldDisplayTabs: Boolean = true) { if (shouldDisplayTabs) tabs.assertDisplayed() else tabs.assertNotDisplayed() - webView.assertDisplayed() + waitForView(allOf(withId(R.id.contentWebView), ViewMatchers.isDisplayed())) Web.onWebView() .withElement(findElement(Locator.TAG_NAME, "html")) .check(WebViewAssertions.webMatches(getText(), comparesEqualTo(syllabusBody))) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 7dafc5a3ab..dd1f97d460 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -46,7 +46,7 @@ import com.instructure.teacher.ui.pages.CourseSettingsPage import com.instructure.teacher.ui.pages.DashboardPage import com.instructure.teacher.ui.pages.DiscussionsDetailsPage import com.instructure.teacher.ui.pages.DiscussionsListPage -import com.instructure.teacher.ui.pages.EditAnnouncementPage +import com.instructure.teacher.ui.pages.EditAnnouncementDetailsPage import com.instructure.teacher.ui.pages.EditAssignmentDetailsPage import com.instructure.teacher.ui.pages.EditDashboardPage import com.instructure.teacher.ui.pages.EditDiscussionsDetailsPage @@ -127,7 +127,7 @@ abstract class TeacherTest : CanvasTest() { val assigneeListPage = AssigneeListPage() val assignmentDetailsPage = AssignmentDetailsPage() val assignmentDueDatesPage = AssignmentDueDatesPage() - val assignmentListPage = AssignmentListPage() + val assignmentListPage = AssignmentListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) val assignmentSubmissionListPage = AssignmentSubmissionListPage() val postSettingsPage = PostSettingsPage() val calendarEventPage = CalendarEventPage() @@ -147,7 +147,7 @@ abstract class TeacherTest : CanvasTest() { val editProfileSettingsPage = EditProfileSettingsPage() val discussionsDetailsPage = DiscussionsDetailsPage() val discussionsListPage = DiscussionsListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) - val editAnnouncementPage = EditAnnouncementPage() + val editAnnouncementDetailsPage = EditAnnouncementDetailsPage() val editAssignmentDetailsPage = EditAssignmentDetailsPage() val editDiscussionsDetailsPage = EditDiscussionsDetailsPage() val editPageDetailsPage = EditPageDetailsPage() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt index ebf1a7182d..83aa31f580 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTestExtensions.kt @@ -21,6 +21,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Environment +import androidx.fragment.app.FragmentActivity import androidx.test.espresso.Espresso import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -390,6 +391,6 @@ fun TeacherTest.routeTo(route: String) { context.startActivity(intent) } -fun TeacherTest.routeTo(route: Route) { - RouteMatcher.route(InstrumentationRegistry.getInstrumentation().targetContext, route) +fun TeacherTest.routeTo(route: Route, activity: FragmentActivity) { + RouteMatcher.route(activity, route) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/adapters/StudentContextFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/adapters/StudentContextFragment.kt index b30556b604..338314e987 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/adapters/StudentContextFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/adapters/StudentContextFragment.kt @@ -145,7 +145,7 @@ class StudentContextFragment : PresenterFragment { - override fun create() = SpeedGraderCommentsPresenter( - rawComments, - submissionHistory, - assignee, - courseId, - assignmentId, - groupMessage, - submissionCommentDao, - attachmentDao, - authorDao, - mediaCommentDao, - pendingSubmissionCommentDao, - fileUploadInputDao, - selectedAttemptId, - assignmentEnhancementsEnabled - ) + override fun create() = SpeedGraderCommentsPresenter( + rawComments, + submissionHistory, + assignee, + courseId, + assignmentId, + groupMessage, + submissionCommentDao, + attachmentDao, + authorDao, + mediaCommentDao, + pendingSubmissionCommentDao, + fileUploadInputDao, + selectedAttemptId, + assignmentEnhancementsEnabled + ) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/event/CalendarEventFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/event/CalendarEventFragment.kt index 6a3cf13b4b..e87d7d7d9d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/event/CalendarEventFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/event/CalendarEventFragment.kt @@ -124,7 +124,7 @@ class CalendarEventFragment : BaseFragment() { loadHtmlJob = calendarEventWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), viewState.htmlContent, { loadCalendarHtml(it, viewState.eventTitle) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepository.kt index 82f6ad7fdc..8f7a466f5f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepository.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepository.kt @@ -27,7 +27,7 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepositor class TeacherEditDashboardRepository(val courseManager: CourseManager) : EditDashboardRepository { - override suspend fun getCurses(): List> { + override suspend fun getCourses(): List> { val courses = courseManager.getCoursesTeacherAsync(true).await().dataOrThrow val filter: (Course, Boolean) -> Boolean = { course, enrollment -> (course.isTeacher || course.isTA || course.isDesigner) && course.hasActiveEnrollment() && enrollment @@ -45,4 +45,8 @@ class TeacherEditDashboardRepository(val courseManager: CourseManager) : EditDas override fun isOpenable(course: Course) = course.isNotDeleted() override fun isFavoriteable(course: Course) = course.isValidTerm() && course.isNotDeleted() && !course.isPastEnrolment() + + override suspend fun getSyncedCourseIds(): Set = emptySet() + + override suspend fun offlineEnabled(): Boolean = false } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt index e518b4414d..1bc4ddea5c 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/dashboard/notifications/TeacherDashboardRouter.kt @@ -40,4 +40,6 @@ class TeacherDashboardRouter(private val activity: FragmentActivity) : Dashboard Route(FileListFragment::class.java, canvasContext, args) ) } + + override fun routeToSyncProgress() = Unit } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt similarity index 94% rename from apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsDetailsFragment.kt rename to apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt index f74a30e151..ddffe62c13 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.instructure.teacher.fragments +package com.instructure.teacher.features.discussion import android.annotation.SuppressLint import android.graphics.Rect @@ -57,6 +57,17 @@ import com.instructure.teacher.dialog.NoInternetConnectionDialog import com.instructure.teacher.events.* import com.instructure.teacher.events.DiscussionEntryEvent import com.instructure.teacher.factory.DiscussionsDetailsPresenterFactory +import com.instructure.teacher.fragments.AssignmentSubmissionListFragment +import com.instructure.teacher.fragments.CreateDiscussionFragment +import com.instructure.teacher.fragments.CreateOrEditAnnouncementFragment +import com.instructure.teacher.fragments.DiscussionBottomSheetChoice +import com.instructure.teacher.fragments.DiscussionBottomSheetMenuFragment +import com.instructure.teacher.fragments.DiscussionsReplyFragment +import com.instructure.teacher.fragments.DiscussionsUpdateFragment +import com.instructure.teacher.fragments.DueDatesFragment +import com.instructure.teacher.fragments.FullscreenInternalWebViewFragment +import com.instructure.teacher.fragments.InternalWebViewFragment +import com.instructure.teacher.fragments.LtiLaunchFragment import com.instructure.teacher.presenters.AssignmentSubmissionListPresenter import com.instructure.teacher.presenters.DiscussionsDetailsPresenter import com.instructure.teacher.router.RouteMatcher @@ -263,7 +274,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< repliesLoadHtmlJob = discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, { discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), html, "text/html", "utf-8", null) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } delay(300) @@ -374,7 +385,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< private fun setupListeners() = with(binding) { dueLayout.setOnClickListener { val args = DueDatesFragment.makeBundle(presenter.discussionTopicHeader.assignment!!) - RouteMatcher.route(requireContext(), Route(null, DueDatesFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, DueDatesFragment::class.java, canvasContext, args)) } submissionsLayout.setOnClickListener { navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, AssignmentSubmissionListPresenter.SubmissionListFilter.ALL) @@ -393,7 +404,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< private fun navigateToSubmissions(context: CanvasContext, assignment: Assignment, filter: AssignmentSubmissionListPresenter.SubmissionListFilter) { val args = AssignmentSubmissionListFragment.makeBundle(assignment, filter) - RouteMatcher.route(requireContext(), Route(null, AssignmentSubmissionListFragment::class.java, context, args)) + RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, context, args)) } private fun loadDiscussionTopicHeader(discussionTopicHeader: DiscussionTopicHeader) = with(binding) { @@ -402,7 +413,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< authorAvatar.setupAvatarA11y(discussionTopicHeader.author?.displayName) authorAvatar.onClick { val bundle = StudentContextFragment.makeBundle(discussionTopicHeader.author?.id ?: 0, canvasContext.id) - RouteMatcher.route(requireContext(), Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(requireActivity(), Route(StudentContextFragment::class.java, null, bundle)) } authorName?.text = discussionTopicHeader.author?.let { Pronouns.span(it.displayName, it.pronouns) } authoredDate?.text = DateHelper.getMonthDayAtTime(requireContext(), discussionTopicHeader.postedDate, getString(R.string.at)) @@ -417,7 +428,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< headerLoadHtmlJob = discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { discussionTopicHeaderWebViewWrapper.loadHtml(it, discussionTopicHeader.title, baseUrl = this@DiscussionsDetailsFragment.discussionTopicHeader.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } discussionRepliesWebViewWrapper.loadHtml("", "") @@ -437,7 +448,11 @@ class DiscussionsDetailsFragment : BasePresenterFragment< if (isAccessibilityEnabled(requireContext()) && discussionTopicHeader.htmlUrl != null) { alternateViewButton.visibility = View.VISIBLE alternateViewButton.setOnClickListener { - val bundle = InternalWebViewFragment.makeBundle(discussionTopicHeader.htmlUrl!!, discussionTopicHeader.title!!, shouldAuthenticate = true) + val bundle = InternalWebViewFragment.makeBundle( + discussionTopicHeader.htmlUrl!!, + discussionTopicHeader.title!!, + shouldAuthenticate = true + ) RouteMatcher.route(requireActivity(), Route(null, InternalWebViewFragment::class.java, canvasContext, bundle)) } } @@ -484,12 +499,12 @@ class DiscussionsDetailsFragment : BasePresenterFragment< if(APIHelper.hasNetworkConnection()) { if(isAnnouncements) { val args = CreateOrEditAnnouncementFragment.newInstanceEdit(presenter.canvasContext, presenter.discussionTopicHeader).nonNullArgs - RouteMatcher.route(requireContext(), Route(CreateOrEditAnnouncementFragment::class.java, null, args)) + RouteMatcher.route(requireActivity(), Route(CreateOrEditAnnouncementFragment::class.java, null, args)) } else { // If we have an assignment, set the topic header to null to prevent cyclic reference presenter.discussionTopicHeader.assignment?.discussionTopicHeader = null val args = CreateDiscussionFragment.makeBundle(presenter.canvasContext, presenter.discussionTopicHeader) - RouteMatcher.route(requireContext(), Route(CreateDiscussionFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(CreateDiscussionFragment::class.java, canvasContext, args)) } } else { NoInternetConnectionDialog.show(requireFragmentManager()) @@ -506,14 +521,13 @@ class DiscussionsDetailsFragment : BasePresenterFragment< webView.settings.javaScriptEnabled = true if(addJSSupport) webView.addJavascriptInterface(JSDiscussionInterface(), "accessor") webView.settings.useWideViewPort = true - webView.settings.allowFileAccess = true webView.settings.loadWithOverviewMode = true CookieManager.getInstance().acceptThirdPartyCookies(webView) webView.canvasWebViewClientCallback = object: CanvasWebView.CanvasWebViewClientCallback { override fun routeInternallyCallback(url: String) { if (!RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, true)) { val bundle = InternalWebViewFragment.makeBundle(url, url, false, "") - RouteMatcher.route(requireContext(), Route(FullscreenInternalWebViewFragment::class.java, + RouteMatcher.route(requireActivity(), Route(FullscreenInternalWebViewFragment::class.java, presenter.canvasContext, bundle)) } } @@ -544,7 +558,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< fun onAvatarPressed(id: String) { presenter.findEntry(id.toLong())?.let { entry -> val bundle = StudentContextFragment.makeBundle(entry.author!!.id, canvasContext.id) - RouteMatcher.route(requireContext(), Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(requireActivity(), Route(StudentContextFragment::class.java, null, bundle)) } } @@ -574,7 +588,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< @JavascriptInterface fun onMoreRepliesPressed(id: String) { val args = makeBundle(presenter.discussionTopicHeader, presenter.discussionTopic, id.toLong(), presenter.getSkipId()) - RouteMatcher.route(requireContext(), Route(null, DiscussionsDetailsFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, DiscussionsDetailsFragment::class.java, canvasContext, args)) } @JavascriptInterface @@ -643,7 +657,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< private fun showReplyView(id: Long) { if (APIHelper.hasNetworkConnection()) { val args = DiscussionsReplyFragment.makeBundle(presenter.discussionTopicHeader.id, id, isAnnouncements) - RouteMatcher.route(requireContext(), Route(DiscussionsReplyFragment::class.java, presenter.canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(DiscussionsReplyFragment::class.java, presenter.canvasContext, args)) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } @@ -666,7 +680,7 @@ class DiscussionsDetailsFragment : BasePresenterFragment< private fun showUpdateReplyView(id: Long) { if (APIHelper.hasNetworkConnection()) { val args = DiscussionsUpdateFragment.makeBundle(presenter.discussionTopicHeader.id, presenter.findEntry(id), isAnnouncements, presenter.discussionTopic) - RouteMatcher.route(requireContext(), Route(DiscussionsUpdateFragment::class.java, presenter.canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(DiscussionsUpdateFragment::class.java, presenter.canvasContext, args)) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } @@ -741,10 +755,10 @@ class DiscussionsDetailsFragment : BasePresenterFragment< if (attachments.size > 1) { AttachmentPickerDialog.show(requireFragmentManager(), attachments) { attachment -> AttachmentPickerDialog.hide(requireFragmentManager()) - attachment.view(requireContext()) + attachment.view(requireActivity()) } } else { - attachments[0].view(requireContext()) + attachments[0].view(requireActivity()) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt new file mode 100644 index 0000000000..d38ebaad48 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt @@ -0,0 +1,34 @@ +package com.instructure.teacher.features.discussion.routing + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository + +class DiscussionRouteHelperTeacherRepository( + private val networkDataSource: DiscussionRouteHelperNetworkDataSource +): DiscussionRouteHelperRepository { + override suspend fun getEnabledFeaturesForCourse( + canvasContext: CanvasContext, + forceNetwork: Boolean + ): Boolean { + return networkDataSource.getEnabledFeaturesForCourse(canvasContext, forceNetwork) + } + + override suspend fun getDiscussionTopicHeader( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + forceNetwork: Boolean + ): DiscussionTopicHeader? { + return networkDataSource.getDiscussionTopicHeader(canvasContext, discussionTopicHeaderId, forceNetwork) + } + + override suspend fun getAllGroups( + discussionTopicHeader: DiscussionTopicHeader, + userId: Long, + forceNetwork: Boolean + ): List { + return networkDataSource.getAllGroups(discussionTopicHeader, userId, forceNetwork) + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/TeacherDiscussionRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt similarity index 84% rename from apps/teacher/src/main/java/com/instructure/teacher/features/discussion/TeacherDiscussionRouter.kt rename to apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt index 21b311f21e..d278a1e5e8 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/TeacherDiscussionRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt @@ -1,4 +1,4 @@ -package com.instructure.teacher.features.discussion +package com.instructure.teacher.features.discussion.routing import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext @@ -8,7 +8,7 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouter import com.instructure.teacher.activities.FullscreenActivity -import com.instructure.teacher.fragments.DiscussionsDetailsFragment +import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.router.RouteMatcher class TeacherDiscussionRouter(private val activity: FragmentActivity) : DiscussionRouter { @@ -21,7 +21,10 @@ class TeacherDiscussionRouter(private val activity: FragmentActivity) : Discussi val route = when { isRedesign -> DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeader) else -> { - val bundle = DiscussionsDetailsFragment.makeBundle(discussionTopicHeader, isAnnouncement || discussionTopicHeader.announcement) + val bundle = DiscussionsDetailsFragment.makeBundle( + discussionTopicHeader, + isAnnouncement || discussionTopicHeader.announcement + ) Route(null, DiscussionsDetailsFragment::class.java, canvasContext, bundle) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/files/search/FileSearchFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/files/search/FileSearchFragment.kt index bdcad88de9..245e39c5cf 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/files/search/FileSearchFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/files/search/FileSearchFragment.kt @@ -50,7 +50,7 @@ class FileSearchFragment : BaseSyncFragment< private val searchAdapter by lazy { FileSearchAdapter(requireContext(), canvasContext.textAndIconColor, presenter) { val editableFile = EditableFile(it, presenter.usageRights, presenter.licenses, canvasContext.backgroundColor, presenter.canvasContext, R.drawable.ic_document) - viewMedia(requireContext(), it.displayName.orEmpty(), it.contentType.orEmpty(), it.url, it.thumbnailUrl, it.displayName, R.drawable.ic_document, canvasContext.backgroundColor, editableFile) + viewMedia(requireActivity(), it.displayName.orEmpty(), it.contentType.orEmpty(), it.url, it.thumbnailUrl, it.displayName, R.drawable.ic_document, canvasContext.backgroundColor, editableFile) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt index 02c0047a19..2263f2e724 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/list/ui/ModuleListView.kt @@ -18,6 +18,7 @@ package com.instructure.teacher.features.modules.list.ui import android.view.LayoutInflater import android.view.ViewGroup +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.FileFolder @@ -137,7 +138,7 @@ class ModuleListView( } else -> null } - RouteMatcher.route(context, route) + RouteMatcher.route(activity as FragmentActivity, route) } fun routeToFile( @@ -155,7 +156,7 @@ class ModuleListView( iconRes = R.drawable.ic_document ) viewMedia( - context = context, + activity = activity as FragmentActivity, filename = file.displayName.orEmpty(), contentType = file.contentType.orEmpty(), url = file.url, diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/offline/sync/TeacherSyncRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/offline/sync/TeacherSyncRouter.kt new file mode 100644 index 0000000000..9b2d95e7b6 --- /dev/null +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/offline/sync/TeacherSyncRouter.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.teacher.features.offline.sync + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import com.instructure.pandautils.features.offline.sync.SyncRouter +import com.instructure.teacher.activities.RouteValidatorActivity + +class TeacherSyncRouter : SyncRouter { + override fun routeToSyncProgress(context: Context): PendingIntent { + val intent = Intent(context, RouteValidatorActivity::class.java) + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } +} \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/postpolicies/ui/PostGradeView.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/postpolicies/ui/PostGradeView.kt index 1e6d89e7ec..197f66a877 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/postpolicies/ui/PostGradeView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/postpolicies/ui/PostGradeView.kt @@ -26,6 +26,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.applyTheme +import com.instructure.pandautils.utils.positionOnScreen import com.instructure.pandautils.utils.setVisible import com.instructure.teacher.R import com.instructure.teacher.databinding.DialogPostGradedEveryoneBinding @@ -84,9 +85,11 @@ class PostGradeView( } val wmlp = dialog.window?.attributes - wmlp?.gravity = Gravity.TOP or Gravity.END - wmlp?.x = (view.x).toInt() - wmlp?.y = (view.y + view.height * 2).toInt() + val (offsetX, offsetY) = binding.postPolicyOnlyGradedSelection.positionOnScreen + + wmlp?.gravity = Gravity.TOP or Gravity.START + wmlp?.x = offsetX + wmlp?.y = offsetY dialog.show() } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusView.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusView.kt index 58be36e568..d087ac93a9 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/syllabus/ui/SyllabusView.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import com.google.android.material.tabs.TabLayout import com.instructure.canvasapi2.models.Assignment @@ -189,17 +190,17 @@ class SyllabusView( } fun showAssignmentView(assignment: Assignment, canvasContext: CanvasContext) { - RouteMatcher.route(context, AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) + RouteMatcher.route(activity as FragmentActivity, AssignmentDetailsFragment.makeRoute(canvasContext, assignment.id)) } fun showScheduleItemView(scheduleItem: ScheduleItem, canvasContext: CanvasContext) { val route = Route(null, CalendarEventFragment::class.java, canvasContext, CalendarEventFragment.createArgs(canvasContext, scheduleItem)) - RouteMatcher.route(context, route) + RouteMatcher.route(activity as FragmentActivity, route) } fun openEditSyllabus(course: Course, summaryAllowed: Boolean) { val fragmentArgs = EditSyllabusFragment.createArgs(course, summaryAllowed) - RouteMatcher.route(context, Route(EditSyllabusFragment::class.java, course, fragmentArgs)) + RouteMatcher.route(activity as FragmentActivity, Route(EditSyllabusFragment::class.java, course, fragmentArgs)) } fun registerEventBus() { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AddMessageFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AddMessageFragment.kt index 1b203c2092..306fab7924 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AddMessageFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AddMessageFragment.kt @@ -37,7 +37,7 @@ import com.instructure.pandautils.dialogs.UnsavedChangesExitDialog import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment import com.instructure.pandautils.features.file.upload.FileUploadDialogParent import com.instructure.pandautils.fragments.BasePresenterFragment -import com.instructure.pandautils.room.common.daos.AttachmentDao +import com.instructure.pandautils.room.appdatabase.daos.AttachmentDao import com.instructure.pandautils.utils.* import com.instructure.pandautils.views.AttachmentView import com.instructure.teacher.R @@ -244,7 +244,7 @@ class AddMessageFragment : BasePresenterFragment viewDiscussionButton.setOnClickListener { - RouteMatcher.route(requireContext(), DiscussionRouterFragment.makeRoute(course, discussionTopicHeader)) + RouteMatcher.route(requireActivity(), DiscussionRouterFragment.makeRoute(course, discussionTopicHeader)) } } ?: viewDiscussionButton.setGone() @@ -401,7 +401,7 @@ class AssignmentDetailsFragment : BasePresenterFragment< private fun openEditPage(assignment: Assignment) { if(APIHelper.hasNetworkConnection()) { val args = EditAssignmentDetailsFragment.makeBundle(assignment, false) - RouteMatcher.route(requireContext(), Route(EditAssignmentDetailsFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(EditAssignmentDetailsFragment::class.java, course, args)) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } @@ -409,7 +409,7 @@ class AssignmentDetailsFragment : BasePresenterFragment< private fun navigateToSubmissions(course: Course, assignment: Assignment, filter: AssignmentSubmissionListPresenter.SubmissionListFilter) { val args = AssignmentSubmissionListFragment.makeBundle(assignment, filter) - RouteMatcher.route(requireContext(), Route(null, AssignmentSubmissionListFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, course, args)) } override fun onResume() { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt index a20e6a382b..bf438e8fd3 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentListFragment.kt @@ -124,14 +124,14 @@ class AssignmentListFragment : BaseExpandableSyncFragment< return AssignmentAdapter(requireContext(), presenter, canvasContext.textAndIconColor) { assignment -> if (pairedWithSubmissions) { val args = AssignmentSubmissionListFragment.makeBundle(assignment) - RouteMatcher.route(requireContext(), Route(null, AssignmentSubmissionListFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, canvasContext, args)) } else { if (assignment.submissionTypesRaw.contains(Assignment.SubmissionType.ONLINE_QUIZ.apiString)) { val args = QuizDetailsFragment.makeBundle(assignment.quizId) - RouteMatcher.route(requireContext(), Route(null, QuizDetailsFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, QuizDetailsFragment::class.java, canvasContext, args)) } else { val args = AssignmentDetailsFragment.makeBundle(assignment) - RouteMatcher.route(requireContext(), Route(null, AssignmentDetailsFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, AssignmentDetailsFragment::class.java, canvasContext, args)) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt index 3e78691768..c6e78b5daa 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AssignmentSubmissionListFragment.kt @@ -134,7 +134,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment< val filteredSubmissions = (0 until presenter.data.size()).map { presenter.data[it] } val selectedIdx = filteredSubmissions.indexOf(gradeableStudentSubmission) val bundle = SpeedGraderActivity.makeBundle(mCourse.id, mAssignment.id, filteredSubmissions, selectedIdx, mAssignment.anonymousGrading) - RouteMatcher.route(requireContext(), Route(bundle, RouteContext.SPEED_GRADER)) + RouteMatcher.route(requireActivity(), Route(bundle, RouteContext.SPEED_GRADER)) } } } @@ -190,7 +190,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment< addMessage.setOnClickListener { val args = AddMessageFragment.createBundle(presenter.getRecipients(), filterTitle.text.toString() + " " + getString(R.string.on) + " " + mAssignment.name, mCourse.contextId, false) - RouteMatcher.route(requireContext(), Route(AddMessageFragment::class.java, null, args)) + RouteMatcher.route(requireActivity(), Route(AddMessageFragment::class.java, null, args)) } } @@ -291,7 +291,7 @@ class AssignmentSubmissionListFragment : BaseSyncFragment< }.show(requireActivity().supportFragmentManager, PeopleListFilterDialog::class.java.simpleName) } R.id.menuPostPolicies -> { - RouteMatcher.route(requireContext(), PostPolicyFragment.makeRoute(mCourse, mAssignment)) + RouteMatcher.route(requireActivity(), PostPolicyFragment.makeRoute(mCourse, mAssignment)) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt index b47e81151a..2d7042273a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/AttendanceListFragment.kt @@ -165,7 +165,7 @@ class AttendanceListFragment : BaseSyncFragment< override fun onAvatarClicked(model: Attendance?, position: Int) { if(model != null && mCanvasContext.id != 0L) { val bundle = StudentContextFragment.makeBundle(model.studentId, mCanvasContext.id, true) - RouteMatcher.route(requireContext(), Route(null, StudentContextFragment::class.java, mCanvasContext, bundle)) + RouteMatcher.route(requireActivity(), Route(null, StudentContextFragment::class.java, mCanvasContext, bundle)) } } }) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt index 46971f5c11..9cc80b09c9 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt @@ -205,7 +205,7 @@ class CourseBrowserFragment : BaseSyncFragment< private val menuItemCallback: (MenuItem) -> Unit = { item -> when (item.itemId) { R.id.menu_course_browser_settings -> { - RouteMatcher.route(requireContext(), Route(CourseSettingsFragment::class.java, presenter.canvasContext)) + RouteMatcher.route(requireActivity(), Route(CourseSettingsFragment::class.java, presenter.canvasContext)) } } } @@ -214,46 +214,46 @@ class CourseBrowserFragment : BaseSyncFragment< return CourseBrowserAdapter(requireActivity(), presenter, presenter.canvasContext.textAndIconColor) { tab -> when (tab.tabId) { Tab.ASSIGNMENTS_ID -> RouteMatcher.route( - requireContext(), + requireActivity(), Route(AssignmentListFragment::class.java, presenter.canvasContext) ) Tab.QUIZZES_ID -> RouteMatcher.route( - requireContext(), + requireActivity(), Route(QuizListFragment::class.java, presenter.canvasContext) ) Tab.DISCUSSIONS_ID -> RouteMatcher.route( - requireContext(), + requireActivity(), Route(DiscussionsListFragment::class.java, presenter.canvasContext) ) Tab.ANNOUNCEMENTS_ID -> RouteMatcher.route( - requireContext(), + requireActivity(), Route(AnnouncementListFragment::class.java, presenter.canvasContext) ) Tab.PEOPLE_ID -> RouteMatcher.route( - requireContext(), + requireActivity(), Route(PeopleListFragment::class.java, presenter.canvasContext) ) Tab.FILES_ID -> { val args = FileListFragment.makeBundle(presenter.canvasContext) RouteMatcher.route( - requireContext(), + requireActivity(), Route(FileListFragment::class.java, presenter.canvasContext, args) ) } Tab.PAGES_ID -> RouteMatcher.route( - requireContext(), + requireActivity(), Route(PageListFragment::class.java, presenter.canvasContext) ) Tab.MODULES_ID -> { val bundle = ModuleListFragment.makeBundle(presenter.canvasContext) - RouteMatcher.route(requireContext(), Route(ModuleListFragment::class.java, presenter.canvasContext, bundle)) + RouteMatcher.route(requireActivity(), Route(ModuleListFragment::class.java, presenter.canvasContext, bundle)) } Tab.STUDENT_VIEW -> { Analytics.logEvent(AnalyticsEventConstants.STUDENT_VIEW_TAPPED) presenter.handleStudentViewClick() } Tab.SYLLABUS_ID -> { - RouteMatcher.route(requireContext(), Route(SyllabusFragment::class.java, presenter.canvasContext, presenter.canvasContext.makeBundle())) + RouteMatcher.route(requireActivity(), Route(SyllabusFragment::class.java, presenter.canvasContext, presenter.canvasContext.makeBundle())) } else -> { if (tab.type == Tab.TYPE_EXTERNAL) { @@ -266,13 +266,13 @@ class CourseBrowserFragment : BaseSyncFragment< if (attendanceExternalToolId.isNotBlank() && attendanceExternalToolId == tab.tabId) { val args = AttendanceListFragment.makeBundle(tab) RouteMatcher.route( - requireContext(), + requireActivity(), Route(AttendanceListFragment::class.java, presenter.canvasContext, args) ) } else { val args = LtiLaunchFragment.makeTabBundle(presenter.canvasContext, tab) RouteMatcher.route( - requireContext(), + requireActivity(), Route(LtiLaunchFragment::class.java, presenter.canvasContext, args) ) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CreateDiscussionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CreateDiscussionFragment.kt index a2199cf8e4..af0d97f848 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CreateDiscussionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CreateDiscussionFragment.kt @@ -448,7 +448,7 @@ class CreateDiscussionFragment : BasePresenterFragment< sectionsMapped.values.toList(), groupsMapped.values.toList(), studentsMapped.values.toList()) - RouteMatcher.route(requireContext(), Route(AssigneeListFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(AssigneeListFragment::class.java, canvasContext, args)) scrollBackToOverride = v } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt index 84d2416abf..9faf698d3e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt @@ -37,6 +37,7 @@ import com.instructure.teacher.databinding.FragmentDiscussionListBinding import com.instructure.teacher.dialog.DiscussionsMoveToDialog import com.instructure.teacher.events.* import com.instructure.teacher.factory.DiscussionListPresenterFactory +import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.presenters.DiscussionListPresenter import com.instructure.teacher.router.RouteMatcher import com.instructure.teacher.utils.RecyclerViewUtils @@ -134,7 +135,7 @@ open class DiscussionsListFragment : BaseExpandableSyncFragment< { discussionTopicHeader -> val route = presenter.getDetailsRoute(discussionTopicHeader) RouteMatcher.route( - requireContext(), + requireActivity(), route ) }, @@ -214,10 +215,10 @@ open class DiscussionsListFragment : BaseExpandableSyncFragment< createNewDiscussion.onClickWithRequireNetwork { if(isAnnouncements) { val args = CreateOrEditAnnouncementFragment.newInstanceCreate(canvasContext).nonNullArgs - RouteMatcher.route(requireContext(), Route(CreateOrEditAnnouncementFragment::class.java, null, args)) + RouteMatcher.route(requireActivity(), Route(CreateOrEditAnnouncementFragment::class.java, null, args)) } else { val args = CreateDiscussionFragment.makeBundle(canvasContext) - RouteMatcher.route(requireContext(), Route(CreateDiscussionFragment::class.java, null, args)) + RouteMatcher.route(requireActivity(), Route(CreateDiscussionFragment::class.java, null, args)) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DueDatesFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DueDatesFragment.kt index 5a8cd7370e..f8b5d1bead 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DueDatesFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DueDatesFragment.kt @@ -86,7 +86,7 @@ class DueDatesFragment : BaseSyncFragment { val args = EditQuizDetailsFragment.makeBundle(assignment.quizId) - RouteMatcher.route(requireContext(), Route(EditQuizDetailsFragment::class.java, mCourse, args)) + RouteMatcher.route(requireActivity(), Route(EditQuizDetailsFragment::class.java, mCourse, args)) } assignment.submissionTypesRaw.contains(Assignment.SubmissionType.DISCUSSION_TOPIC.apiString) -> { val discussionTopicHeader = assignment.discussionTopicHeader @@ -94,11 +94,11 @@ class DueDatesFragment : BaseSyncFragment { val args = EditAssignmentDetailsFragment.makeBundle(assignment, true) - RouteMatcher.route(requireContext(), Route(EditAssignmentDetailsFragment::class.java, mCourse, args)) + RouteMatcher.route(requireActivity(), Route(EditAssignmentDetailsFragment::class.java, mCourse, args)) } } } else { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt index 0ff0f03708..89e9c31a94 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt @@ -378,7 +378,7 @@ class EditAssignmentDetailsFragment : BaseFragment() { sectionsMapped.values.toList(), groupsMapped.values.toList(), studentsMapped.values.toList()) - RouteMatcher.route(requireContext(), Route(AssigneeListFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(AssigneeListFragment::class.java, course, args)) scrollBackToOverride = v } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt index c2d3618094..4fb8c596ab 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt @@ -394,7 +394,7 @@ class EditQuizDetailsFragment : BasePresenterFragment< sectionsMapped.values.toList(), groupsMapped.values.toList(), studentsMapped.values.toList()) - RouteMatcher.route(requireContext(), Route(AssigneeListFragment::class.java, mCourse, args)) + RouteMatcher.route(requireActivity(), Route(AssigneeListFragment::class.java, mCourse, args)) scrollBackToOverride = v } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt index 56c13981e8..99985270b1 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt @@ -211,12 +211,12 @@ class FileListFragment : BaseSyncFragment< val bundle = ViewHtmlFragment.makeAuthSessionBundle(canvasContext, it, it.displayName.orEmpty(), canvasContext.backgroundColor, editableFile) RouteMatcher.route(requireActivity(), Route(ViewHtmlFragment::class.java, null, bundle)) } else { - viewMedia(requireContext(), it.displayName.orEmpty(), it.contentType.orEmpty(), it.url, it.thumbnailUrl, it.displayName, R.drawable.ic_document, canvasContext.backgroundColor, editableFile) + viewMedia(requireActivity(), it.displayName.orEmpty(), it.contentType.orEmpty(), it.url, it.thumbnailUrl, it.displayName, R.drawable.ic_document, canvasContext.backgroundColor, editableFile) } } else { // This is a folder - val args = FileListFragment.makeBundle(presenter.mCanvasContext, it) - RouteMatcher.route(requireContext(), Route(FileListFragment::class.java, presenter.mCanvasContext, args)) + val args = makeBundle(presenter.mCanvasContext, it) + RouteMatcher.route(requireActivity(), Route(FileListFragment::class.java, presenter.mCanvasContext, args)) } } } @@ -309,9 +309,9 @@ class FileListFragment : BaseSyncFragment< when (it.itemId) { R.id.edit -> { val bundle = EditFileFolderFragment.makeBundle(presenter.currentFolder, presenter.usageRights, presenter.licenses, presenter.mCanvasContext.id) - RouteMatcher.route(requireContext(), Route(EditFileFolderFragment::class.java, canvasContext, bundle)) + RouteMatcher.route(requireActivity(), Route(EditFileFolderFragment::class.java, canvasContext, bundle)) } - R.id.search -> RouteMatcher.route(requireContext(), Route(FileSearchFragment::class.java, canvasContext, Bundle())) + R.id.search -> RouteMatcher.route(requireActivity(), Route(FileSearchFragment::class.java, canvasContext, Bundle())) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt index 222856ccb4..c8224a11c0 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt @@ -17,13 +17,13 @@ package com.instructure.teacher.fragments import android.app.Activity -import android.content.Context import android.net.Uri import android.os.Bundle import android.os.Handler import android.view.View import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Tab @@ -194,9 +194,9 @@ class LtiLaunchFragment : BaseFragment() { fun newInstance(args: Bundle) = LtiLaunchFragment().apply { arguments = args } - fun routeLtiLaunchFragment(context: Context, canvasContext: CanvasContext?, url: String) { - val args = makeBundle(canvasContext, URLDecoder.decode(url, "utf-8"), context.getString(R.string.utils_externalToolTitle), true) - RouteMatcher.route(context, Route(LtiLaunchFragment::class.java, canvasContext, args)) + fun routeLtiLaunchFragment(activity: FragmentActivity, canvasContext: CanvasContext?, url: String) { + val args = makeBundle(canvasContext, URLDecoder.decode(url, "utf-8"), activity.getString(R.string.utils_externalToolTitle), true) + RouteMatcher.route(activity, Route(LtiLaunchFragment::class.java, canvasContext, args)) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/MessageThreadFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/MessageThreadFragment.kt index 016979126e..cbd77836be 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/MessageThreadFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/MessageThreadFragment.kt @@ -130,13 +130,13 @@ class MessageThreadFragment : BaseSyncFragment { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt index cde474614a..1e807fedb2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt @@ -192,7 +192,7 @@ class PageDetailsFragment : BasePresenterFragment< loadHtmlJob = binding.canvasWebViewWraper.webView.loadHtmlWithIframes(requireContext(), page.body, { if (view != null) binding.canvasWebViewWraper.loadHtml(it, page.title, baseUrl = this.page.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } setupToolbar() } @@ -220,7 +220,7 @@ class PageDetailsFragment : BasePresenterFragment< private fun openEditPage(page: Page) { if (APIHelper.hasNetworkConnection()) { val args = CreateOrEditPageDetailsFragment.newInstanceEdit(canvasContext, page).nonNullArgs - RouteMatcher.route(requireContext(), Route(CreateOrEditPageDetailsFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(CreateOrEditPageDetailsFragment::class.java, canvasContext, args)) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageListFragment.kt index 483824a24c..10750e48f5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageListFragment.kt @@ -129,7 +129,7 @@ class PageListFragment : BaseSyncFragment val args = PageDetailsFragment.makeBundle(page) - RouteMatcher.route(requireContext(), Route(null, PageDetailsFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, PageDetailsFragment::class.java, canvasContext, args)) } } @@ -181,7 +181,7 @@ class PageListFragment : BaseSyncFragment { if(APIHelper.hasNetworkConnection()) { - RouteMatcher.route(requireContext(), Route(ProfileEditFragment::class.java, ApiPrefs.user)) + RouteMatcher.route(requireActivity(), Route(ProfileEditFragment::class.java, ApiPrefs.user)) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt index bd25246c10..bc74da67ce 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizDetailsFragment.kt @@ -382,7 +382,7 @@ class QuizDetailsFragment : BasePresenterFragment< loadHtmlJob = instructionsWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), quiz.description, { instructionsWebViewWrapper.loadHtml(it, quiz.title, baseUrl = this@QuizDetailsFragment.quiz.htmlUrl) }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireContext(), canvasContext, it) + LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) } } @@ -390,7 +390,7 @@ class QuizDetailsFragment : BasePresenterFragment< dueLayout.setOnClickListener { if(quiz._assignment != null) { val args = DueDatesFragment.makeBundle(quiz._assignment!!) - RouteMatcher.route(requireContext(), Route(null, DueDatesFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(null, DueDatesFragment::class.java, course, args)) } } @@ -419,7 +419,7 @@ class QuizDetailsFragment : BasePresenterFragment< val uri = URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref) urlStr = uri.toASCIIString() val args = QuizPreviewWebviewFragment.makeBundle(urlStr, getString(R.string.quizPreview)) - RouteMatcher.route(requireContext(), Route(QuizPreviewWebviewFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(QuizPreviewWebviewFragment::class.java, course, args)) } catch (e: UnsupportedEncodingException) {} } } @@ -428,7 +428,7 @@ class QuizDetailsFragment : BasePresenterFragment< assignment ?: return // We can't navigate to the submission list if there isn't an associated assignment val assignmentWithAnonymousGrading = assignment.copy(anonymousGrading = quiz.allowAnonymousSubmissions) val args = AssignmentSubmissionListFragment.makeBundle(assignmentWithAnonymousGrading, filter) - RouteMatcher.route(requireContext(), Route(null, AssignmentSubmissionListFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, course, args)) } private fun clearListeners() = with(binding) { @@ -443,7 +443,7 @@ class QuizDetailsFragment : BasePresenterFragment< private fun openEditPage(quiz: Quiz) { if(APIHelper.hasNetworkConnection()) { val args = EditQuizDetailsFragment.makeBundle(quiz, false) - RouteMatcher.route(requireContext(), Route(EditQuizDetailsFragment::class.java, course, args)) + RouteMatcher.route(requireActivity(), Route(EditQuizDetailsFragment::class.java, course, args)) } else { NoInternetConnectionDialog.show(requireFragmentManager()) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt index f6ffe4d71f..35305fe673 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt @@ -120,7 +120,7 @@ class QuizListFragment : BaseExpandableSyncFragment< RouteMatcher.routeUrl(requireActivity(), quiz.htmlUrl!!, ApiPrefs.domain) } else { val args = QuizDetailsFragment.makeBundle(quiz) - RouteMatcher.route(requireContext(), Route(null, QuizDetailsFragment::class.java, canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(null, QuizDetailsFragment::class.java, canvasContext, args)) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt index 250435f453..efb93c2465 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SettingsFragment.kt @@ -55,7 +55,7 @@ class SettingsFragment : BasePresenterFragment attachment.view(requireContext()) } + private val onAttachmentClicked = { attachment: Attachment -> attachment.view(requireActivity()) } private val commentLibraryViewModel: CommentLibraryViewModel by activityViewModels() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt index 966a673b90..f18fca18f1 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderLtiSubmissionFragment.kt @@ -56,7 +56,7 @@ class SpeedGraderLtiSubmissionFragment : Fragment() { ViewStyler.themeButton(binding.viewLtiButton) binding.viewLtiButton.onClick { val args = InternalWebViewFragment.makeBundle(mUrl, getString(R.string.canvasAPI_externalTool)) - RouteMatcher.route(requireContext(), Route(InternalWebViewFragment::class.java, mCanvasContext, args)) + RouteMatcher.route(requireActivity(), Route(InternalWebViewFragment::class.java, mCanvasContext, args)) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizSubmissionFragment.kt index e7e58e09d3..c16e016ad9 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderQuizSubmissionFragment.kt @@ -89,7 +89,7 @@ class SpeedGraderQuizSubmissionFragment : Fragment() { private fun viewQuizSubmission() { val bundle = SpeedGraderQuizWebViewFragment.newInstance(mCourseId, mAssignmentId, mStudentId, mUrl).nonNullArgs - RouteMatcher.route(requireContext(), Route(SpeedGraderQuizWebViewFragment::class.java, null, bundle)) + RouteMatcher.route(requireActivity(), Route(SpeedGraderQuizWebViewFragment::class.java, null, bundle)) } companion object { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ToDoFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ToDoFragment.kt index 8d119c269f..4795d7bfec 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ToDoFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ToDoFragment.kt @@ -154,7 +154,7 @@ class ToDoFragment : BaseSyncFragment { val args = EditFileFolderFragment.makeBundle(it.file, it.usageRights, it.licenses, it.canvasContext!!.id) - RouteMatcher.route(requireContext(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) } R.id.copyLink -> { if(it.file.url != null) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt index fc10c97fcf..1cd5cc518a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewImageFragment.kt @@ -85,7 +85,7 @@ class ViewImageFragment : Fragment(), ShareableFile { when (menu.itemId) { R.id.edit -> { val args = EditFileFolderFragment.makeBundle(it.file, it.usageRights, it.licenses, it.canvasContext!!.id) - RouteMatcher.route(requireContext(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) } R.id.copyLink -> { if(it.file.url != null) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt index 6774f6ba96..3e140d2edc 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewMediaFragment.kt @@ -82,7 +82,7 @@ class ViewMediaFragment : Fragment(), ShareableFile { speedGraderMediaPlayerView.findViewById(R.id.fullscreenButton).onClick { mExoAgent.flagForResume() val bundle = BaseViewMediaActivity.makeBundle(mUri.toString(), mThumbnailUrl, mContentType, mDisplayName, false) - RouteMatcher.route(requireContext(), Route(bundle, RouteContext.MEDIA)) + RouteMatcher.route(requireActivity(), Route(bundle, RouteContext.MEDIA)) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewPdfFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewPdfFragment.kt index dcb8cff567..990c9cc12e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewPdfFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewPdfFragment.kt @@ -90,7 +90,7 @@ class ViewPdfFragment : PresenterFragment { val args = EditFileFolderFragment.makeBundle(it.file, it.usageRights, it.licenses, it.canvasContext!!.id) - RouteMatcher.route(requireContext(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) } R.id.copyLink -> { if(it.file.url != null) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewUnsupportedFileFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewUnsupportedFileFragment.kt index 70e81c1f3c..ec0e9df8d5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewUnsupportedFileFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/ViewUnsupportedFileFragment.kt @@ -90,7 +90,7 @@ class ViewUnsupportedFileFragment : Fragment() { when (menu.itemId) { R.id.edit -> { val args = EditFileFolderFragment.makeBundle(it.file, it.usageRights, it.licenses, it.canvasContext!!.id) - RouteMatcher.route(requireContext(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) + RouteMatcher.route(requireActivity(), Route(EditFileFolderFragment::class.java, it.canvasContext, args)) } R.id.copyLink -> { if(it.file.url != null) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/holders/GradeableStudentSubmissionViewHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/holders/GradeableStudentSubmissionViewHolder.kt index 43a6877538..a303e8eee0 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/holders/GradeableStudentSubmissionViewHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/holders/GradeableStudentSubmissionViewHolder.kt @@ -22,6 +22,7 @@ import android.util.TypedValue import android.view.View import android.view.accessibility.AccessibilityNodeInfo import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.GradeableStudentSubmission @@ -73,7 +74,7 @@ class GradeableStudentSubmissionViewHolder(private val binding: AdapterGradeable studentAvatar.setupAvatarA11y(assignee.name) studentAvatar.onClick { val bundle = StudentContextFragment.makeBundle(assignee.id, courseId) - RouteMatcher.route(context, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(context as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) } } assignee is GroupAssignee -> { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt b/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt index b0ef9e8b8f..f42843bd3a 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/holders/SpeedGraderCommentHolder.kt @@ -17,6 +17,7 @@ package com.instructure.teacher.holders import android.widget.ImageView +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.postmodels.CommentSendStatus @@ -82,7 +83,7 @@ class SpeedGraderCommentHolder(private val binding: AdapterSubmissionCommentBind avatarView.setupAvatarA11y(comment.authorName) avatarView.onClick { val bundle = StudentContextFragment.makeBundle(comment.authorId, courseId) - RouteMatcher.route(context, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(context as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) } } Triple( @@ -142,7 +143,7 @@ class SpeedGraderCommentHolder(private val binding: AdapterSubmissionCommentBind avatarView.setupAvatarA11y(assignee.name) avatarView.onClick { val bundle = StudentContextFragment.makeBundle(assignee.id, courseId) - RouteMatcher.route(context, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(context as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) } Triple(null, Pronouns.span(assignee.name, assignee.pronouns), assignee.student.avatarUrl) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt index 8056ab960d..c92cdb2341 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt @@ -31,7 +31,7 @@ import com.instructure.canvasapi2.models.postmodels.FileSubmitObject import com.instructure.pandautils.utils.MediaUploadUtils import com.instructure.teacher.events.DiscussionTopicHeaderDeletedEvent import com.instructure.teacher.events.post -import com.instructure.teacher.fragments.DiscussionsDetailsFragment +import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.interfaces.RceMediaUploadPresenter import com.instructure.teacher.viewinterface.CreateDiscussionView import instructure.androidblueprint.FragmentPresenter diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt index e24243f73d..a0c7b93ab7 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt @@ -35,7 +35,7 @@ import com.instructure.teacher.events.DiscussionCreatedEvent import com.instructure.teacher.events.DiscussionTopicHeaderDeletedEvent import com.instructure.teacher.events.DiscussionUpdatedEvent import com.instructure.teacher.events.post -import com.instructure.teacher.fragments.DiscussionsDetailsFragment +import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.interfaces.RceMediaUploadPresenter import com.instructure.teacher.viewinterface.CreateOrEditAnnouncementView import instructure.androidblueprint.FragmentPresenter diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderCommentsPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderCommentsPresenter.kt index fbda9eddc2..71191aed4b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderCommentsPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/SpeedGraderCommentsPresenter.kt @@ -32,11 +32,7 @@ import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker import com.instructure.pandautils.room.appdatabase.daos.* import com.instructure.pandautils.room.appdatabase.entities.FileUploadInputEntity import com.instructure.pandautils.room.appdatabase.entities.PendingSubmissionCommentEntity -import com.instructure.pandautils.room.common.model.SubmissionCommentWithAttachments -import com.instructure.pandautils.room.common.daos.AttachmentDao -import com.instructure.pandautils.room.common.daos.AuthorDao -import com.instructure.pandautils.room.common.daos.MediaCommentDao -import com.instructure.pandautils.room.common.daos.SubmissionCommentDao +import com.instructure.pandautils.room.appdatabase.model.SubmissionCommentWithAttachments import com.instructure.teacher.events.SubmissionCommentsUpdated import com.instructure.teacher.events.SubmissionUpdatedEvent import com.instructure.teacher.events.post diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index dc71ffe16f..08c8c05553 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -16,7 +16,6 @@ */ package com.instructure.teacher.router -import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.os.Bundle @@ -50,6 +49,7 @@ import com.instructure.teacher.PSPDFKit.AnnotationComments.AnnotationCommentList import com.instructure.teacher.R import com.instructure.teacher.activities.* import com.instructure.teacher.adapters.StudentContextFragment +import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment import com.instructure.teacher.features.syllabus.edit.EditSyllabusFragment @@ -82,14 +82,26 @@ object RouteMatcher : BaseRouteMatcher() { routes.add(Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/assignments"), AssignmentListFragment::class.java)) - routes.add(Route(courseOrGroup("/:course_id/assignments/:assignment_id"), AssignmentListFragment::class.java, AssignmentDetailsFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:course_id/assignments/:assignment_id"), + AssignmentListFragment::class.java, + AssignmentDetailsFragment::class.java + ) + ) routes.add(Route(courseOrGroup("/:course_id/assignments/:assignment_id/submissions/:submission_id"), RouteContext.SPEED_GRADER)) routes.add(Route(courseOrGroup("/:course_id/quizzes"), QuizListFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/quizzes/:quiz_id"), QuizListFragment::class.java, QuizDetailsFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/discussion_topics"), DiscussionsListFragment::class.java)) - routes.add(Route(courseOrGroup("/:course_id/discussion_topics/:message_id"), DiscussionsListFragment::class.java, DiscussionRouterFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:course_id/discussion_topics/:message_id"), + DiscussionsListFragment::class.java, + DiscussionRouterFragment::class.java + ) + ) routes.add(Route(courseOrGroup("/:course_id/files"), FileListFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/files/:file_id/download"), RouteContext.FILE)) @@ -111,7 +123,13 @@ object RouteMatcher : BaseRouteMatcher() { routes.add(Route(courseOrGroup("/:course_id/wiki/:page_id/"), PageListFragment::class.java, PageDetailsFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/announcements"), AnnouncementListFragment::class.java)) - routes.add(Route(courseOrGroup("/:course_id/announcements/:message_id"), AnnouncementListFragment::class.java, DiscussionRouterFragment::class.java)) + routes.add( + Route( + courseOrGroup("/:course_id/announcements/:message_id"), + AnnouncementListFragment::class.java, + DiscussionRouterFragment::class.java + ) + ) } private fun initClassMap() { @@ -148,49 +166,54 @@ object RouteMatcher : BaseRouteMatcher() { bottomSheetFragments.add(EditFileFolderFragment::class.java) bottomSheetFragments.add(CreateOrEditPageDetailsFragment::class.java) bottomSheetFragments.add(EditSyllabusFragment::class.java) + bottomSheetFragments.add(PostPolicyFragment::class.java) } - private fun routeUrl(context: Context, url: String) { - routeUrl(context, url, ApiPrefs.domain) + private fun routeUrl(activity: FragmentActivity, url: String) { + routeUrl(activity, url, ApiPrefs.domain) } - fun routeUrl(context: Context, url: String, domain: String) { + fun routeUrl(activity: FragmentActivity, url: String, domain: String) { /* Possible activity types we can navigate to: Unknown Link, InitActivity, Master/Detail, Fullscreen, WebView, ViewMedia */ // Find the best route // Pass that along to the activity // One or two classes? (F, or M/D) - route(context, getInternalRoute(url, domain)) + route(activity, getInternalRoute(url, domain)) } - fun route(context: Context, route: Route?) { + fun route(activity: FragmentActivity, route: Route?) { if (route == null || route.routeContext === RouteContext.DO_NOT_ROUTE) { if (route?.uri != null) { //No route, no problem - handleWebViewUrl(context, route.uri.toString()) + handleWebViewUrl(activity, route.uri.toString()) } - } else if (route.routeContext == RouteContext.FILE || route.primaryClass?.isAssignableFrom(FileListFragment::class.java) == true && route.queryParamsHash.containsKey(RouterParams.PREVIEW)) { + } else if (route.routeContext == RouteContext.FILE + || route.primaryClass?.isAssignableFrom(FileListFragment::class.java) == true + && route.queryParamsHash.containsKey(RouterParams.PREVIEW) + ) { if (route.queryParamsHash.containsKey(RouterParams.VERIFIER) && route.queryParamsHash.containsKey(RouterParams.DOWNLOAD_FRD)) { if (route.uri != null) { - openMedia(context as FragmentActivity, route.uri.toString()) + openMedia(activity, route.uri.toString()) } } else { handleSpecificFile( - context as FragmentActivity, - (if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) route.queryParamsHash[RouterParams.PREVIEW] else route.paramsHash[RouterParams.FILE_ID]) ?: "", - route) + activity, + (if (route.queryParamsHash.containsKey(RouterParams.PREVIEW)) route.queryParamsHash[RouterParams.PREVIEW] else route.paramsHash[RouterParams.FILE_ID]).orEmpty(), + route + ) } } else if (route.routeContext === RouteContext.MEDIA) { - handleMediaRoute(context, route) + handleMediaRoute(activity, route) } else if (route.routeContext === RouteContext.SPEED_GRADER) { - handleSpeedGraderRoute(context, route) - } else if (context.resources.getBoolean(R.bool.isDeviceTablet)) { - handleTabletRoute(context, route) + handleSpeedGraderRoute(activity, route) + } else if (activity.resources.getBoolean(R.bool.isDeviceTablet)) { + handleTabletRoute(activity, route) } else { - handleFullscreenRoute(context, route) + handleFullscreenRoute(activity, route) } } @@ -201,13 +224,13 @@ object RouteMatcher : BaseRouteMatcher() { * @param routeIfPossible * @return */ - fun canRouteInternally(context: Context?, url: String?, domain: String, routeIfPossible: Boolean): Boolean { + fun canRouteInternally(activity: FragmentActivity?, url: String?, domain: String, routeIfPossible: Boolean): Boolean { if (url.isNullOrBlank()) return false val canRoute = getInternalRoute(url, domain) != null - if (canRoute && context != null && routeIfPossible) { - routeUrl(context, url) + if (canRoute && activity != null && routeIfPossible) { + routeUrl(activity, url) } return canRoute } @@ -329,22 +352,30 @@ object RouteMatcher : BaseRouteMatcher() { when { ProfileFragment::class.java.isAssignableFrom(cls) -> fragment = ProfileFragment() CourseBrowserFragment::class.java.isAssignableFrom(cls) -> fragment = CourseBrowserFragment.newInstance((canvasContext as Course?)!!) - CourseBrowserEmptyFragment::class.java.isAssignableFrom(cls) -> fragment = CourseBrowserEmptyFragment.newInstance((canvasContext as Course?)!!) + CourseBrowserEmptyFragment::class.java.isAssignableFrom(cls) -> fragment = CourseBrowserEmptyFragment + .newInstance((canvasContext as Course?)!!) DashboardFragment::class.java.isAssignableFrom(cls) -> fragment = DashboardFragment.getInstance() - AssignmentListFragment::class.java.isAssignableFrom(cls) -> fragment = AssignmentListFragment.getInstance(canvasContext!!, route.arguments) + AssignmentListFragment::class.java.isAssignableFrom(cls) -> fragment = AssignmentListFragment + .getInstance(canvasContext!!, route.arguments) AssignmentDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = getAssignmentDetailsFragment(canvasContext, route) - DueDatesFragment::class.java.isAssignableFrom(cls) -> fragment = DueDatesFragment.getInstance((canvasContext as Course?)!!, route.arguments) - AssignmentSubmissionListFragment::class.java.isAssignableFrom(cls) -> fragment = AssignmentSubmissionListFragment.newInstance((canvasContext as Course?)!!, route.arguments) + DueDatesFragment::class.java.isAssignableFrom(cls) -> fragment = DueDatesFragment + .getInstance((canvasContext as Course?)!!, route.arguments) + AssignmentSubmissionListFragment::class.java.isAssignableFrom(cls) -> fragment = AssignmentSubmissionListFragment + .newInstance((canvasContext as Course?)!!, route.arguments) PostPolicyFragment::class.java.isAssignableFrom(cls) -> fragment = PostPolicyFragment.newInstance(route.argsWithContext) - EditAssignmentDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditAssignmentDetailsFragment.newInstance((canvasContext as Course?)!!, route.arguments) + EditAssignmentDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditAssignmentDetailsFragment + .newInstance((canvasContext as Course?)!!, route.arguments) AssigneeListFragment::class.java.isAssignableFrom(cls) -> fragment = AssigneeListFragment.newInstance(route.arguments) CourseSettingsFragment::class.java.isAssignableFrom(cls) -> fragment = CourseSettingsFragment.newInstance((canvasContext as Course?)!!) QuizListFragment::class.java.isAssignableFrom(cls) -> fragment = QuizListFragment.newInstance(canvasContext!!) QuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = getQuizDetailsFragment(canvasContext, route) - EditQuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditQuizDetailsFragment.newInstance((canvasContext as Course?)!!, route.arguments) + EditQuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditQuizDetailsFragment + .newInstance((canvasContext as Course?)!!, route.arguments) QuizPreviewWebviewFragment::class.java.isAssignableFrom(cls) -> fragment = QuizPreviewWebviewFragment.newInstance(route.arguments) - EditQuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditQuizDetailsFragment.newInstance((canvasContext as Course?)!!, route.arguments) - AnnouncementListFragment::class.java.isAssignableFrom(cls) -> fragment = AnnouncementListFragment.newInstance(canvasContext!!) // This needs to be above DiscussionsListFragment because it extends it + EditQuizDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = EditQuizDetailsFragment + .newInstance((canvasContext as Course?)!!, route.arguments) + AnnouncementListFragment::class.java.isAssignableFrom(cls) -> fragment = AnnouncementListFragment + .newInstance(canvasContext!!) // This needs to be above DiscussionsListFragment because it extends it DiscussionsListFragment::class.java.isAssignableFrom(cls) -> fragment = DiscussionsListFragment.newInstance(canvasContext!!) DiscussionsDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = getDiscussionDetailsFragment(canvasContext, route) DiscussionDetailsWebViewFragment::class.java.isAssignableFrom(cls) -> fragment = DiscussionDetailsWebViewFragment.newInstance(route) @@ -357,25 +388,32 @@ object RouteMatcher : BaseRouteMatcher() { ViewMediaFragment::class.java.isAssignableFrom(cls) -> fragment = ViewMediaFragment.newInstance(route.arguments) ViewHtmlFragment::class.java.isAssignableFrom(cls) -> fragment = ViewHtmlFragment.newInstance(route.arguments) ViewUnsupportedFileFragment::class.java.isAssignableFrom(cls) -> fragment = ViewUnsupportedFileFragment.newInstance(route.arguments) - cls.isAssignableFrom(DiscussionsReplyFragment::class.java) -> fragment = DiscussionsReplyFragment.newInstance(canvasContext!!, route.arguments) - cls.isAssignableFrom(DiscussionsUpdateFragment::class.java) -> fragment = DiscussionsUpdateFragment.newInstance(canvasContext!!, route.arguments) + cls.isAssignableFrom(DiscussionsReplyFragment::class.java) -> fragment = DiscussionsReplyFragment + .newInstance(canvasContext!!, route.arguments) + cls.isAssignableFrom(DiscussionsUpdateFragment::class.java) -> fragment = DiscussionsUpdateFragment + .newInstance(canvasContext!!, route.arguments) ChooseRecipientsFragment::class.java.isAssignableFrom(cls) -> fragment = ChooseRecipientsFragment.newInstance(route.arguments) SpeedGraderQuizWebViewFragment::class.java.isAssignableFrom(cls) -> fragment = SpeedGraderQuizWebViewFragment.newInstance(route.arguments) AnnotationCommentListFragment::class.java.isAssignableFrom(cls) -> fragment = AnnotationCommentListFragment.newInstance(route.arguments) CreateDiscussionFragment::class.java.isAssignableFrom(cls) -> fragment = CreateDiscussionFragment.newInstance(route.arguments) - CreateOrEditAnnouncementFragment::class.java.isAssignableFrom(cls) -> fragment = CreateOrEditAnnouncementFragment.newInstance(route.arguments) + CreateOrEditAnnouncementFragment::class.java.isAssignableFrom(cls) -> fragment = CreateOrEditAnnouncementFragment + .newInstance(route.arguments) SettingsFragment::class.java.isAssignableFrom(cls) -> fragment = SettingsFragment.newInstance(route.arguments) ProfileEditFragment::class.java.isAssignableFrom(cls) -> fragment = ProfileEditFragment.newInstance(route.arguments) LtiLaunchFragment::class.java.isAssignableFrom(cls) -> fragment = LtiLaunchFragment.newInstance(route.arguments) PeopleListFragment::class.java.isAssignableFrom(cls) -> fragment = PeopleListFragment.newInstance(canvasContext!!) StudentContextFragment::class.java.isAssignableFrom(cls) -> fragment = StudentContextFragment.newInstance(route.arguments) - AttendanceListFragment::class.java.isAssignableFrom(cls) -> fragment = AttendanceListFragment.newInstance(canvasContext!!, route.arguments) - FileListFragment::class.java.isAssignableFrom(cls) -> fragment = FileListFragment.newInstance(canvasContext ?: route.canvasContext!!, route.arguments) + AttendanceListFragment::class.java.isAssignableFrom(cls) -> fragment = AttendanceListFragment + .newInstance(canvasContext!!, route.arguments) + FileListFragment::class.java.isAssignableFrom(cls) -> fragment = FileListFragment + .newInstance(canvasContext ?: route.canvasContext!!, route.arguments) PageListFragment::class.java.isAssignableFrom(cls) -> fragment = PageListFragment.newInstance(canvasContext!!) PageDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = getPageDetailsFragment(canvasContext, route) EditFileFolderFragment::class.java.isAssignableFrom(cls) -> fragment = EditFileFolderFragment.newInstance(route.arguments) - CreateOrEditPageDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = CreateOrEditPageDetailsFragment.newInstance(route.arguments) - FullscreenInternalWebViewFragment::class.java.isAssignableFrom(cls) -> fragment = FullscreenInternalWebViewFragment.newInstance(route.arguments) + CreateOrEditPageDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = CreateOrEditPageDetailsFragment + .newInstance(route.arguments) + FullscreenInternalWebViewFragment::class.java.isAssignableFrom(cls) -> fragment = FullscreenInternalWebViewFragment + .newInstance(route.arguments) InternalWebViewFragment::class.java.isAssignableFrom(cls) -> fragment = InternalWebViewFragment.newInstance(route.arguments) HtmlContentFragment::class.java.isAssignableFrom(cls) -> fragment = HtmlContentFragment.newInstance(route.arguments) } //NOTE: These should remain at or near the bottom to give fragments that extend InternalWebViewFragment the chance first @@ -385,8 +423,7 @@ object RouteMatcher : BaseRouteMatcher() { private fun getMessageThreadFragment(route: Route): Fragment? { return if (route.paramsHash.containsKey(Const.CONVERSATION_ID)) { - val args = MessageThreadFragment.createBundle(route.paramsHash[Const.CONVERSATION_ID]?.toLong() - ?: 0L) + val args = MessageThreadFragment.createBundle(route.paramsHash[Const.CONVERSATION_ID]?.toLong() ?: 0L) MessageThreadFragment.newInstance(args) } else { MessageThreadFragment.newInstance(route.arguments) @@ -433,7 +470,10 @@ object RouteMatcher : BaseRouteMatcher() { private fun getDiscussionDetailsFragment(canvasContext: CanvasContext?, route: Route): DiscussionsDetailsFragment { return when { - route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER) -> DiscussionsDetailsFragment.newInstance(canvasContext!!, route.arguments) + route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER) -> DiscussionsDetailsFragment.newInstance( + canvasContext!!, + route.arguments + ) route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER_ID) -> { val discussionTopicHeaderId = route.arguments.getLong(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER_ID) val args = DiscussionsDetailsFragment.makeBundle(discussionTopicHeaderId) @@ -460,7 +500,7 @@ object RouteMatcher : BaseRouteMatcher() { } } - private fun getLoaderCallbacks(activity: Activity): LoaderManager.LoaderCallbacks { + private fun getLoaderCallbacks(activity: FragmentActivity): LoaderManager.LoaderCallbacks { if (openMediaCallbacks == null) { openMediaCallbacks = object : LoaderManager.LoaderCallbacks { override fun onCreateLoader(id: Int, args: Bundle?): Loader { @@ -471,25 +511,46 @@ object RouteMatcher : BaseRouteMatcher() { try { if (loadedMedia.isError) { if (loadedMedia.errorType == OpenMediaAsyncTaskLoader.ErrorType.NO_APPS) { - val args = ViewUnsupportedFileFragment.newInstance(loadedMedia.intent!!.data!!, (loader as OpenMediaAsyncTaskLoader).filename!!, loadedMedia.intent!!.type!!, null, R.drawable.ic_attachment).nonNullArgs - RouteMatcher.route(activity, Route(ViewUnsupportedFileFragment::class.java, null, args)) + val args = ViewUnsupportedFileFragment.newInstance( + loadedMedia.intent!!.data!!, + (loader as OpenMediaAsyncTaskLoader).filename!!, + loadedMedia.intent!!.type!!, + null, + R.drawable.ic_attachment + ).nonNullArgs + route(activity, Route(ViewUnsupportedFileFragment::class.java, null, args)) } else { Toast.makeText(activity, activity.resources.getString(loadedMedia.errorMessage), Toast.LENGTH_LONG).show() } } else if (loadedMedia.isHtmlFile) { - val args = ViewHtmlFragment.makeDownloadBundle(loadedMedia.bundle!!.getString(Const.INTERNAL_URL)!!, loadedMedia.bundle!!.getString(Const.ACTION_BAR_TITLE)!!) - RouteMatcher.route(activity, Route(ViewHtmlFragment::class.java, null, args)) + val args = ViewHtmlFragment.makeDownloadBundle( + loadedMedia.bundle!!.getString(Const.INTERNAL_URL)!!, + loadedMedia.bundle!!.getString(Const.ACTION_BAR_TITLE)!! + ) + route(activity, Route(ViewHtmlFragment::class.java, null, args)) } else if (loadedMedia.intent != null) { if (loadedMedia.intent?.type?.contains("pdf") == true && !loadedMedia.isUseOutsideApps) { // Show pdf with PSPDFkit val args = ViewPdfFragment.newInstance((loader as OpenMediaAsyncTaskLoader).url, 0).nonNullArgs - RouteMatcher.route(activity, Route(ViewPdfFragment::class.java, null, args)) + route(activity, Route(ViewPdfFragment::class.java, null, args)) } else if (loadedMedia.intent?.type == "video/mp4") { - val bundle = BaseViewMediaActivity.makeBundle(loadedMedia.intent!!.data!!.toString(), null, "video/mp4", loadedMedia.intent!!.dataString, true) - RouteMatcher.route(activity, Route(bundle, RouteContext.MEDIA)) + val bundle = BaseViewMediaActivity.makeBundle( + loadedMedia.intent!!.data!!.toString(), + null, + "video/mp4", + loadedMedia.intent!!.dataString, + true + ) + route(activity, Route(bundle, RouteContext.MEDIA)) } else if (loadedMedia.intent?.type?.startsWith("image/") == true) { - val args = ViewImageFragment.newInstance(loadedMedia.intent!!.dataString!!, loadedMedia.intent!!.data!!, "image/*", true, 0).nonNullArgs - RouteMatcher.route(activity, Route(ViewImageFragment::class.java, null, args)) + val args = ViewImageFragment.newInstance( + loadedMedia.intent!!.dataString!!, + loadedMedia.intent!!.data!!, + "image/*", + true, + 0 + ).nonNullArgs + route(activity, Route(ViewImageFragment::class.java, null, args)) } else { activity.startActivity(loadedMedia.intent) } @@ -512,7 +573,8 @@ object RouteMatcher : BaseRouteMatcher() { openMediaCallbacks = null openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(url, fileName) LoaderUtils.restartLoaderWithBundle>( - LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID) + LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID + ) } } @@ -527,23 +589,35 @@ object RouteMatcher : BaseRouteMatcher() { if (filename.lowercase(Locale.getDefault()).endsWith(".htm") || filename.lowercase(Locale.getDefault()).endsWith(".html")) { RouteUtils.retrieveFileUrl(route, fileId) { fileUrl, context, needsAuth -> val bundle = InternalWebViewFragment.makeBundle(url = fileUrl, title = filename, shouldAuthenticate = needsAuth) - RouteMatcher.route(activity, Route(FullscreenInternalWebViewFragment::class.java, context, bundle)) + route(activity, Route(FullscreenInternalWebViewFragment::class.java, context, bundle)) } } else { openMediaCallbacks = null openMediaBundle = OpenMediaAsyncTaskLoader.createBundle(mime, url, filename) - LoaderUtils.restartLoaderWithBundle>( - LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID) + LoaderUtils.restartLoaderWithBundle( + LoaderManager.getInstance(activity), openMediaBundle, getLoaderCallbacks(activity), R.id.openMediaLoaderID + ) } } private fun handleSpecificFile(activity: FragmentActivity, fileID: String?, route: Route) { val fileFolderStatusCallback = object : StatusCallback() { - override fun onResponse(response: retrofit2.Response, linkHeaders: com.instructure.canvasapi2.utils.LinkHeaders, type: ApiType) { + override fun onResponse( + response: retrofit2.Response, + linkHeaders: LinkHeaders, + type: ApiType + ) { super.onResponse(response, linkHeaders, type) val fileFolder = response.body() if (fileFolder!!.isLocked || fileFolder.isLockedForUser) { - Toast.makeText(activity, String.format(activity.getString(R.string.fileLocked), if (fileFolder.displayName == null) activity.getString(R.string.file) else fileFolder.displayName), Toast.LENGTH_LONG).show() + Toast.makeText( + activity, + String.format( + activity.getString(R.string.fileLocked), + if (fileFolder.displayName == null) activity.getString(R.string.file) else fileFolder.displayName + ), + Toast.LENGTH_LONG + ).show() } else { openMedia(activity, fileFolder.contentType!!, fileFolder.url!!, fileFolder.displayName!!, route, fileID) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt index ede2ce176b..de15b7559f 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt @@ -18,6 +18,7 @@ import com.instructure.pandautils.utils.argsWithContext import com.instructure.teacher.PSPDFKit.AnnotationComments.AnnotationCommentListFragment import com.instructure.teacher.adapters.StudentContextFragment import com.instructure.teacher.features.calendar.event.CalendarEventFragment +import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.features.files.search.FileSearchFragment import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment diff --git a/apps/teacher/src/main/java/com/instructure/teacher/tasks/TeacherLogoutTask.kt b/apps/teacher/src/main/java/com/instructure/teacher/tasks/TeacherLogoutTask.kt index 1f439f2cd2..13879f1c02 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/tasks/TeacherLogoutTask.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/tasks/TeacherLogoutTask.kt @@ -50,4 +50,6 @@ class TeacherLogoutTask(type: Type, uri: Uri? = null) : LogoutTask(type, uri) { listener(registrationId) } } + + override fun removeOfflineData(userId: Long?) {} } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/ModelExtensions.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/ModelExtensions.kt index 915f33605f..6791ff2319 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/ModelExtensions.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/ModelExtensions.kt @@ -17,9 +17,9 @@ @file:JvmName("ModelExtensions") package com.instructure.teacher.utils -import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap +import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.type.EnrollmentType @@ -55,8 +55,8 @@ fun Attachment.asMediaSubmissionPlaceholder(submission: Submission?): Attachment val Attachment.isMediaSubmissionPlaceholder: Boolean get() = id == MEDIA_PLACEHOLDER_ID @JvmName("viewAttachment") -fun Attachment.view(context: Context) { - viewMedia(context, filename!!, contentType!!, url, thumbnailUrl, displayName, iconRes, fullScreen = true) +fun Attachment.view(activity: FragmentActivity) { + viewMedia(activity, filename!!, contentType!!, url, thumbnailUrl, displayName, iconRes, fullScreen = true) } /** @@ -65,41 +65,52 @@ fun Attachment.view(context: Context) { * file list) */ -fun viewMedia(context: Context, filename: String, contentType: String, url: String?, thumbnailUrl: String?, displayName: String?, iconRes: Int, toolbarColor: Int = 0, editableFile: EditableFile? = null, fullScreen: Boolean = false) { +fun viewMedia( + activity: FragmentActivity, + filename: String, + contentType: String, + url: String?, + thumbnailUrl: String?, + displayName: String?, + iconRes: Int, + toolbarColor: Int = 0, + editableFile: EditableFile? = null, + fullScreen: Boolean = false +) { val extension = filename.substringAfterLast('.') when { - // PDF + // PDF contentType == "application/pdf" -> { PdfFragment() val bundle = ViewPdfFragment.newInstance(url ?: "", toolbarColor, editableFile).nonNullArgs if (fullScreen) { - RouteMatcher.route(context, Route(ViewPdfFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(ViewPdfFragment::class.java, null, bundle)) } else { - RouteMatcher.route(context, Route(null, ViewPdfFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(null, ViewPdfFragment::class.java, null, bundle)) } } // Audio/Video contentType.startsWith("video") || contentType.startsWith("audio") -> { val bundle = BaseViewMediaActivity.makeBundle(url.orEmpty(), thumbnailUrl, contentType, displayName, true, editableFile) - RouteMatcher.route(context, Route(bundle, RouteContext.MEDIA)) + RouteMatcher.route(activity, Route(bundle, RouteContext.MEDIA)) } // Image contentType.startsWith("image") -> { val title = displayName ?: filename val bundle = ViewImageFragment.newInstance(title, Uri.parse(url), contentType, true, toolbarColor, editableFile).nonNullArgs if (fullScreen) { - RouteMatcher.route(context, Route(ViewImageFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(ViewImageFragment::class.java, null, bundle)) } else { - RouteMatcher.route(context, Route(null, ViewImageFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(null, ViewImageFragment::class.java, null, bundle)) } } // HTML contentType == "text/html" || extension == "htm" || extension == "html" -> { val bundle = ViewHtmlFragment.makeDownloadBundle(url ?: "", filename, toolbarColor, editableFile) if (fullScreen) { - RouteMatcher.route(context, Route(ViewHtmlFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(ViewHtmlFragment::class.java, null, bundle)) } else { - RouteMatcher.route(context, Route(null, ViewHtmlFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(null, ViewHtmlFragment::class.java, null, bundle)) } } // Multipart (Unknown) @@ -107,17 +118,17 @@ fun viewMedia(context: Context, filename: String, contentType: String, url: Stri //Discover the actual type of file val type: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) if(type != null && type != "multipart/form-data") { - viewMedia(context, filename, type, url, thumbnailUrl, displayName, iconRes, toolbarColor, editableFile) + viewMedia(activity, filename, type, url, thumbnailUrl, displayName, iconRes, toolbarColor, editableFile) } else { // Other val bundle = ViewUnsupportedFileFragment.newInstance(Uri.parse(url), filename, contentType, tryOrNull { Uri.parse(thumbnailUrl) }, iconRes, toolbarColor, editableFile).nonNullArgs - RouteMatcher.route(context, Route(null, ViewUnsupportedFileFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(null, ViewUnsupportedFileFragment::class.java, null, bundle)) } } // Other else -> { val bundle = ViewUnsupportedFileFragment.newInstance(Uri.parse(url), filename, contentType, tryOrNull { Uri.parse(thumbnailUrl) }, iconRes, toolbarColor, editableFile).nonNullArgs - RouteMatcher.route(context, Route(null, ViewUnsupportedFileFragment::class.java, null, bundle)) + RouteMatcher.route(activity, Route(null, ViewUnsupportedFileFragment::class.java, null, bundle)) } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/utils/ViewExtensions.kt b/apps/teacher/src/main/java/com/instructure/teacher/utils/ViewExtensions.kt index 3d6bfbd01a..0544c818fe 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/utils/ViewExtensions.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/utils/ViewExtensions.kt @@ -32,6 +32,7 @@ import androidx.annotation.MenuRes import androidx.annotation.StringRes import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.interactions.MasterDetailInteractions import com.instructure.pandautils.utils.isTablet @@ -234,7 +235,7 @@ class DefensiveURLSpan(private val url: String) : URLSpan(url) { override fun onClick(widget: View) { if(RouteMatcher.getInternalRoute(url, ApiPrefs.domain) != null) { - RouteMatcher.routeUrl(widget.context, url, ApiPrefs.domain) + RouteMatcher.routeUrl(widget.context as FragmentActivity, url, ApiPrefs.domain) } else { val intent = InternalWebViewActivity.createIntent(widget.context, url, "", false) widget.context.startActivity(intent) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt index cee0f920fd..c73d2e4130 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/SubmissionContentView.kt @@ -32,6 +32,7 @@ import androidx.appcompat.widget.ListPopupWindow import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import androidx.viewpager.widget.ViewPager @@ -156,7 +157,7 @@ class SubmissionContentView( if(context.isTablet) { setIsCurrentlyAnnotating(true) } - RouteMatcher.route(context, Route(AnnotationCommentListFragment::class.java, null, bundle)) + RouteMatcher.route(activity as FragmentActivity, Route(AnnotationCommentListFragment::class.java, null, bundle)) } @SuppressLint("CommitTransaction") @@ -509,7 +510,7 @@ class SubmissionContentView( userImageView.setupAvatarA11y(assignee.name) userImageView.onClick { val bundle = StudentContextFragment.makeBundle(assignee.id, mCourse.id) - RouteMatcher.route(context, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(activity as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) } } } @@ -528,7 +529,7 @@ class SubmissionContentView( } } R.id.menuPostPolicies -> { - RouteMatcher.route(context, PostPolicyFragment.makeRoute(mCourse, mAssignment)) + RouteMatcher.route(activity as FragmentActivity, PostPolicyFragment.makeRoute(mCourse, mAssignment)) } } } @@ -553,7 +554,7 @@ class SubmissionContentView( popup.isModal = true // For a11y popup.setOnItemClickListener { _, _, position, _ -> val bundle = StudentContextFragment.makeBundle(assignee.students[position].id, mCourse.id) - RouteMatcher.route(context, Route(StudentContextFragment::class.java, null, bundle)) + RouteMatcher.route(activity as FragmentActivity, Route(StudentContextFragment::class.java, null, bundle)) popup.dismiss() } popup.show() @@ -785,7 +786,7 @@ class SubmissionContentView( } floatingRecordingView.replayCallback = { val bundle = BaseViewMediaActivity.makeBundle(it, "video", context.getString(R.string.videoCommentReplay), true) - RouteMatcher.route(context, Route(bundle, RouteContext.MEDIA)) + RouteMatcher.route(activity as FragmentActivity, Route(bundle, RouteContext.MEDIA)) } } diff --git a/apps/teacher/src/main/res/layout/dialog_post_graded_everyone.xml b/apps/teacher/src/main/res/layout/dialog_post_graded_everyone.xml index 3da9a4f056..b7b06a7338 100644 --- a/apps/teacher/src/main/res/layout/dialog_post_graded_everyone.xml +++ b/apps/teacher/src/main/res/layout/dialog_post_graded_everyone.xml @@ -19,21 +19,19 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:elevation="6dp" - app:cardCornerRadius="3dp" - android:clipChildren="true" - android:clipToPadding="true"> + app:cardCornerRadius="3dp">
diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepositoryTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepositoryTest.kt index 19a434634b..2b4c58a8b3 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepositoryTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/dashboard/edit/TeacherEditDashboardRepositoryTest.kt @@ -28,6 +28,7 @@ import io.mockk.mockk import io.mockk.mockkStatic import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Before import org.junit.Test @@ -77,7 +78,7 @@ class TeacherEditDashboardRepositoryTest { } // When - val result = repository.getCurses() + val result = repository.getCourses() // Then val expected = listOf(listOf(courseActive), listOf(courseCompleted), listOf(courseInvitedOrPending)) @@ -137,4 +138,22 @@ class TeacherEditDashboardRepositoryTest { // Then Assert.assertFalse(result) } + + @Test + fun `Synced course ids always returns empty set`() = runTest { + // When + val result = repository.getSyncedCourseIds() + + // Then + Assert.assertEquals(emptySet(), result) + } + + @Test + fun `Offline enabled always returns false`() = runTest { + // When + val result = repository.offlineEnabled() + + // Then + Assert.assertFalse(result) + } } \ No newline at end of file diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt new file mode 100644 index 0000000000..a1609fd6c5 --- /dev/null +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt @@ -0,0 +1,51 @@ +package com.instructure.teacher.features.discussion.routing + +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class DiscussionRouteHelperTeacherRepositoryTest { + private val networkDataSource: DiscussionRouteHelperNetworkDataSource = mockk(relaxed = true) + + private val repository = DiscussionRouteHelperTeacherRepository(networkDataSource) + + @Test + fun `getEnabledFeaturesForCourse() calls networkDataSource`() = runTest { + val expected = true + + coEvery { networkDataSource.getEnabledFeaturesForCourse(any(), any()) } returns expected + + val result = repository.getEnabledFeaturesForCourse(mockk(), true) + + assertEquals(expected, result) + } + + @Test + fun `getDiscussionTopicHeader() calls networkDataSource`() = runTest { + val expected = DiscussionTopicHeader(1L) + + coEvery { networkDataSource.getDiscussionTopicHeader(any(), any(), any()) } returns expected + + val result = repository.getDiscussionTopicHeader(mockk(), 1L, true) + + assertEquals(expected, result) + } + + @Test + fun `getAllGroups() calls networkDataSource`() = runTest { + val expected = listOf(Group(1L)) + + coEvery { networkDataSource.getAllGroups(any(), any(), any()) } returns expected + + val result = repository.getAllGroups(mockk(), 1L, true) + + assertEquals(expected, result) + } +} \ No newline at end of file diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/CoursesApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/CoursesApi.kt index dbbfd36533..521f12a152 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/CoursesApi.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/CoursesApi.kt @@ -18,6 +18,7 @@ package com.instructure.dataseeding.api import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.dataseeding.model.CourseSettings import com.instructure.dataseeding.model.CreateCourse import com.instructure.dataseeding.model.CreateCourseWrapper import com.instructure.dataseeding.model.FavoriteApiModel @@ -27,7 +28,9 @@ import retrofit2.Call import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path +import retrofit2.http.QueryMap object CoursesApi { interface CoursesService { @@ -43,6 +46,10 @@ object CoursesApi { @DELETE("courses/{courseId}?event=conclude") fun concludeCourse(@Path("courseId") courseId: Long): Call + + @PUT("courses/{course_id}/settings") + fun updateCourseSettings(@Path("course_id") courseId: Long, @QueryMap params: Map): Call + } private val adminCoursesService: CoursesService by lazy { @@ -112,4 +119,12 @@ object CoursesApi { .addCourseToFavorites(courseId) .execute() .body()!! + + fun updateCourseSettings(courseId: Long, params: Map + ): CourseSettings { + return adminCoursesService + .updateCourseSettings(courseId, params) + .execute() + .body()!! + } } diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/CourseApiModel.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/CourseApiModel.kt index 3ed7922df7..db75704ec1 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/CourseApiModel.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/model/CourseApiModel.kt @@ -45,9 +45,15 @@ data class CreateCourse( @SerializedName("account_id") val accountId: Long? = null, @SerializedName("syllabus_body") - val syllabusBody: String? = null + val syllabusBody: String? = null, + @SerializedName("settings") + val settings: CourseSettings? = null ) +data class CourseSettings( + @SerializedName("restrict_quantitative_data") + var restrictQuantitativeData: Boolean = false, +) data class CreateCourseWrapper( val course: CreateCourse, diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index ae7ed73795..185d7aa675 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -296,11 +296,6 @@ abstract class CanvasTest : InstructureTestingContract { } } - // Does the test device have particularly low screen resolution? - fun isLowResDevice() : Boolean { - return activityRule.activity.resources.displayMetrics.densityDpi < DisplayMetrics.DENSITY_HIGH - } - fun isTabletDevice(): Boolean { val metrics = activityRule.activity.resources.displayMetrics @@ -482,6 +477,10 @@ abstract class CanvasTest : InstructureTestingContract { return getDeviceOrientation(ApplicationProvider.getApplicationContext()) == Configuration.ORIENTATION_PORTRAIT } + // Does the test device have particularly low screen resolution? + fun isLowResDevice() : Boolean { + return ApplicationProvider.getApplicationContext().resources.displayMetrics.densityDpi < DisplayMetrics.DENSITY_HIGH + } } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt new file mode 100644 index 0000000000..bd8864ec0c --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/OfflineE2E.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.instructure.canvas.espresso + +// When applied to a test method, denotes that the test is the part of the Offline mode test case suite. +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class OfflineE2E(val explanation: String = "") \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt index 90080e9df1..9a4b28306c 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/StubMultiAPILevel.kt @@ -19,4 +19,4 @@ package com.instructure.canvas.espresso // Apply on a test method which is failing on Firebase Test Lab (FTL) on some API levels. Write the failing API Levels into the parameter. @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) -annotation class StubMultiAPILevel(val failedApiLevels: String = "") \ No newline at end of file +annotation class StubMultiAPILevel(val failedApiLevels: String = "", val explanation: String = "") \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index cc3230943c..9b559020bb 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -310,6 +310,8 @@ class MockCanvas { } } + var offlineModeEnabled = false + companion object { /** Whether the mock Canvas data has been initialized for the current test run */ val isInitialized: Boolean get() = ::data.isInitialized diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt index 62d81e572c..222d390b54 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ApiEndpoint.kt @@ -142,6 +142,17 @@ object ApiEndpoint : Endpoint( request.successResponse(plannerOverride!!) } } + ), + Segment("features") to Endpoint( + Segment("environment") to object : Endpoint( + response = { + GET { + request.successResponse(mapOf("mobile_offline_mode" to data.offlineModeEnabled)) + } + } + ) { + override val authModel = DontCareAuthModel + } ) ) diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt index a9c27b5bc9..92a6c1f62d 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/CustomViewAssertions.kt @@ -77,20 +77,20 @@ class NotificationBadgeAssertion(@IdRes private val menuItemId: Int, private val } } -class DoesNotExistAssertion(private val timeout: Long, private val pollInterval: Long = 500L) : ViewAssertion { +class DoesNotExistAssertion(private val timeoutInSeconds: Long, private val pollIntervalInSeconds: Long = 1L) : ViewAssertion { override fun check(view: View?, noViewFoundException: NoMatchingViewException?) { var elapsedTime = 0L - while (elapsedTime < timeout) { + while (elapsedTime < timeoutInSeconds * 1000) { try { doesNotExist() return } catch (e: AssertionFailedError) { - Thread.sleep(pollInterval) - elapsedTime += pollInterval + Thread.sleep(pollIntervalInSeconds * 1000) + elapsedTime += (pollIntervalInSeconds * 1000) } } - throw AssertionError("View still exists after $timeout milliseconds.") + throw AssertionError("View still exists after $timeoutInSeconds seconds.") } } \ No newline at end of file diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 0a7371b326..4370c14f67 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -32,7 +32,7 @@ object Versions { const val HILT_ANDROIDX = "1.0.0" const val LIFECYCLE = "2.6.0" const val FRAGMENT = "1.5.5" - const val WORK_MANAGER = "2.7.1" + const val WORK_MANAGER = "2.8.1" const val GLIDE_VERSION = "4.15.1" const val RETROFIT = "2.9.0" const val OKHTTP = "4.10.0" diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt index f7b938031e..a7dd52b401 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AnnouncementAPI.kt @@ -22,22 +22,30 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query +import retrofit2.http.Tag import retrofit2.http.Url object AnnouncementAPI { - internal interface AnnouncementInterface { + interface AnnouncementInterface { @GET("{contextType}/{contextId}/discussion_topics?only_announcements=1&include[]=sections") fun getFirstPageAnnouncementsList(@Path("contextType") contextType: String, @Path("contextId") contextId: Long): Call> + @GET("{contextType}/{contextId}/discussion_topics?only_announcements=1&include[]=sections") + suspend fun getFirstPageAnnouncementsList(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag params: RestParams): DataResult> + @GET fun getNextPageAnnouncementsList(@Url nextUrl: String): Call> + @GET + fun getNextPageAnnouncementsList(@Url nextUrl: String, @Tag params: RestParams): DataResult> + /** * This API call returns the latest announcement. The current implementation is the latest announcement from the last 14 days. */ diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt index c3290f6973..5bc48d8b7e 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/AssignmentAPI.kt @@ -22,24 +22,55 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.postmodels.AssignmentPostBodyWrapper +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.* object AssignmentAPI { - internal interface AssignmentInterface { + interface AssignmentInterface { @GET("courses/{courseId}/external_tools/sessionless_launch") fun getExternalToolLaunchUrl(@Path("courseId") courseId: Long, @Query("id") externalToolId: Long, @Query("assignment_id") assignmentId: Long, @Query("launch_type") launchType: String = "assessment"): Call + @GET("courses/{courseId}/external_tools/sessionless_launch") + suspend fun getExternalToolLaunchUrl( + @Path("courseId") courseId: Long, + @Query("id") externalToolId: Long, + @Query("assignment_id") assignmentId: Long, + @Query("launch_type") launchType: String = "assessment", + @Tag restParams: RestParams + ): DataResult + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics") fun getAssignment(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics") + suspend fun getAssignment( + @Path("courseId") courseId: Long, + @Path("assignmentId") assignmentId: Long, + @Tag params: RestParams + ): DataResult + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") fun getAssignmentWithHistory(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call - @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics[]=submission_history") + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=score_statistics&include[]=submission_history") + suspend fun getAssignmentWithHistory( + @Path("courseId") courseId: Long, + @Path("assignmentId") assignmentId: Long, + @Tag restParams: RestParams + ): DataResult + + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") fun getAssignmentIncludeObservees(@Path("courseId") courseId: Long, @Path("assignmentId") assignmentId: Long): Call + @GET("courses/{courseId}/assignments/{assignmentId}?include[]=submission&include[]=rubric_assessment&needs_grading_count_by_section=true&override_assignment_dates=true&all_dates=true&include[]=overrides&include[]=observed_users&include[]=score_statistics&include[]=submission_history") + suspend fun getAssignmentIncludeObservees( + @Path("courseId") courseId: Long, + @Path("assignmentId") assignmentId: Long, + @Tag restParams: RestParams + ): DataResult + @GET("courses/{courseId}/assignment_groups/{assignmentGroupId}") fun getAssignmentGroup( @Path("courseId") courseId: Long, @@ -48,16 +79,37 @@ object AssignmentAPI { @GET("courses/{courseId}/assignment_groups?include[]=assignments&include[]=discussion_topic&include[]=submission&override_assignment_dates=true&include[]=all_dates&include[]=overrides") fun getFirstPageAssignmentGroupListWithAssignments(@Path("courseId") courseId: Long): Call> + @GET("courses/{courseId}/assignment_groups?include[]=assignments&include[]=discussion_topic&include[]=submission&include[]=rubric_assessment&override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=submission_history&include[]=submission_comments&include[]=score_statistics") + suspend fun getFirstPageAssignmentGroupListWithAssignments(@Path("courseId") courseId: Long, @Tag restParams: RestParams): DataResult> + @GET fun getNextPageAssignmentGroupListWithAssignments(@Url nextUrl: String): Call> + @GET + suspend fun getNextPageAssignmentGroupListWithAssignments(@Url nextUrl: String, @Tag restParams: RestParams): DataResult> + // https://canvas.instructure.com/doc/api/all_resources.html#method.submissions_api.for_students @GET("courses/{courseId}/assignment_groups?include[]=assignments&include[]=discussion_topic&include[]=submission&override_assignment_dates=true&include[]=all_dates&include[]=overrides") fun getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod(@Path("courseId") courseId: Long, @Query("grading_period_id") gradingPeriodId: Long, @Query("scope_assignments_to_student") scopeToStudent: Boolean, @Query("order") order: String = "id"): Call> + @GET("courses/{courseId}/assignment_groups?include[]=assignments&include[]=discussion_topic&include[]=submission&override_assignment_dates=true&include[]=all_dates&include[]=overrides") + suspend fun getFirstPageAssignmentGroupListWithAssignmentsForGradingPeriod( + @Path("courseId") courseId: Long, + @Query("grading_period_id") gradingPeriodId: Long, + @Query("scope_assignments_to_student") scopeToStudent: Boolean, + @Query("order") order: String = "id", + @Tag restParams: RestParams + ): DataResult> + @GET fun getNextPageAssignmentGroupListWithAssignmentsForGradingPeriod(@Url nextUrl: String): Call> + @GET + suspend fun getNextPageAssignmentGroupListWithAssignmentsForGradingPeriod( + @Url nextUrl: String, + @Tag restParams: RestParams + ): DataResult> + @GET fun getNextPage(@Url nextUrl: String): Call> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt index 757794f821..84eff20309 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt @@ -21,6 +21,7 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.Response import retrofit2.http.* @@ -29,7 +30,7 @@ import java.io.IOException object CalendarEventAPI { - internal interface CalendarEventInterface { + interface CalendarEventInterface { @get:GET("users/self/upcoming_events") val upcomingEvents: Call> @@ -42,9 +43,21 @@ object CalendarEventAPI { @Query("end_date") endDate: String?, @Query(value = "context_codes[]", encoded = true) contextCodes: List): Call> + @GET("calendar_events/") + suspend fun getCalendarEvents( + @Query("all_events") allEvents: Boolean, + @Query("type") type: String, + @Query("start_date") startDate: String?, + @Query("end_date") endDate: String?, + @Query(value = "context_codes[]", encoded = true) contextCodes: List, @Tag restParams: RestParams + ): DataResult> + @GET fun next(@Url url: String): Call> + @GET + suspend fun next(@Url url: String, @Tag restParams: RestParams): DataResult> + @GET("calendar_events/{eventId}") fun getCalendarEvent(@Path("eventId") eventId: Long): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ConferencesApi.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ConferencesApi.kt index d81af50f8a..4e0698c4f7 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ConferencesApi.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ConferencesApi.kt @@ -20,22 +20,33 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ConferenceList +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Tag import retrofit2.http.Url object ConferencesApi { - internal interface ConferencesInterface { + interface ConferencesInterface { @GET("{canvasContext}/conferences") fun getConferencesForContext(@Path("canvasContext", encoded = true) canvasContext: String): Call + @GET("{canvasContext}/conferences") + suspend fun getConferencesForContext( + @Path("canvasContext", encoded = true) canvasContext: String, + @Tag params: RestParams + ): DataResult + @GET("conferences?state=live") fun getLiveConferences(): Call @GET fun getNextPage(@Url nextUrl: String): Call + + @GET + suspend fun getNextPage(@Url nextUrl: String, @Tag params: RestParams): DataResult } fun getConferencesForContext( diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index 9ebfd03b96..712e9d1d4d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -39,16 +39,19 @@ object CourseAPI { @get:GET("dashboard/dashboard_cards") val dashboardCourses: Call> + @GET("dashboard/dashboard_cards") + suspend fun getDashboardCourses(@Tag params: RestParams): DataResult> + @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=banner_image&include[]=sections&include[]=settings&state[]=completed&state[]=available") val firstPageCourses: Call> @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=banner_image&include[]=sections&include[]=settings&state[]=completed&state[]=available&include[]=grading_scheme") val firstPageCoursesWithGradingScheme: Call> - @GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=banner_image&include[]=sections&state[]=completed&state[]=available") + @GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=banner_image&include[]=sections&state[]=completed&state[]=available&include[]=tabs&include[]=settings") suspend fun getFirstPageCourses(@Tag params: RestParams): DataResult> - @GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available&state[]=unpublished") + @GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=settings&state[]=completed&state[]=available&state[]=unpublished") suspend fun getFirstPageCoursesTeacher(@Tag params: RestParams): DataResult> @get:GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded") @@ -57,6 +60,9 @@ object CourseAPI { @get:GET("courses?include[]=term&include[]=syllabus_body&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available&include[]=observed_users&include[]=settings&include[]=grading_scheme") val firstPageCoursesWithSyllabus: Call> + @GET("courses?include[]=term&include[]=syllabus_body&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available&include[]=observed_users&include[]=settings") + suspend fun firstPageCoursesWithSyllabus(@Tag params: RestParams): DataResult> + @get:GET("courses?include[]=term&include[]=syllabus_body&include[]=license&include[]=is_public&include[]=permissions&enrollment_state=active") val firstPageCoursesWithSyllabusWithActiveEnrollment: Call> @@ -66,20 +72,35 @@ object CourseAPI { @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=course_image") fun getCourse(@Path("courseId") courseId: Long): Call + @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=course_image&include[]=tabs") + suspend fun getCourse(@Path("courseId") courseId: Long, @Tag params: RestParams): DataResult + @GET("courses/{courseId}/settings") fun getCourseSettings(@Path("courseId") courseId: Long): Call + @GET("courses/{courseId}/settings") + suspend fun getCourseSettings(@Path("courseId") courseId: Long, @Tag restParams: RestParams): DataResult + @PUT("courses/{course_id}/settings") fun updateCourseSettings(@Path("course_id") courseId: Long, @QueryMap params: Map): Call @GET("courses/{courseId}?include[]=syllabus_body&include[]=term&include[]=license&include[]=is_public&include[]=permissions") fun getCourseWithSyllabus(@Path("courseId") courseId: Long): Call + @GET("courses/{courseId}?include[]=syllabus_body&include[]=term&include[]=license&include[]=is_public&include[]=permissions") + suspend fun getCourseWithSyllabus(@Path("courseId") courseId: Long, @Tag restParams: RestParams): DataResult + @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=total_scores&include[]=current_grading_period_scores&include[]=course_image&include[]=settings&include[]=grading_scheme") fun getCourseWithGrade(@Path("courseId") courseId: Long): Call + @GET("courses/{courseId}?include[]=term&include[]=permissions&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=total_scores&include[]=current_grading_period_scores&include[]=course_image&include[]=settings&include[]=grading_scheme") + suspend fun getCourseWithGrade(@Path("courseId") courseId: Long, @Tag restParams: RestParams): DataResult + + @GET("courses/{courseId}?include[]=term&include[]=syllabus_body&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&include[]=public_description&include[]=grading_periods&include[]=account&include[]=course_progress&include[]=storage_quota_used_mb&include[]=total_students&include[]=passback_status&include[]=teachers&include[]=tabs&include[]=banner_image&include[]=concluded&include[]=observed_users&include[]=settings&include[]=grading_scheme") + suspend fun getFullCourseContent(@Path("courseId") courseId: Long, @Tag restParams: RestParams): DataResult + @GET("courses?include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=current_and_concluded") - fun firstPageCoursesByEnrollmentState(@Query("enrollment_state") enrollmentState: String): Call> + suspend fun firstPageCoursesByEnrollmentState(@Query("enrollment_state") enrollmentState: String, @Tag params: RestParams): DataResult> @GET fun next(@Url nextURL: String): Call> @@ -94,6 +115,9 @@ object CourseAPI { @GET("courses/{courseId}/grading_periods?per_page=100") fun getGradingPeriodsForCourse(@Path("courseId") courseId: Long): Call + @GET("courses/{courseId}/grading_periods?per_page=100") + suspend fun getGradingPeriodsForCourse(@Path("courseId") courseId: Long, @Tag params: RestParams): DataResult + @GET("courses/{courseId}/users/{studentId}?include[]=avatar_url&include[]=enrollments&include[]=inactive_enrollments&include[]=current_grading_period_scores&include[]=email") fun getCourseStudent(@Path("courseId") courseId: Long, @Path("studentId") studentId: Long): Call @@ -118,9 +142,20 @@ object CourseAPI { @GET("courses/{courseId}/permissions") fun getCoursePermissions(@Path("courseId") courseId: Long, @Query("permissions[]") requestedPermissions: List): Call + @GET("courses/{courseId}/permissions") + suspend fun getCoursePermissions(@Path("courseId") courseId: Long, @Query("permissions[]") requestedPermissions: List, @Tag params: RestParams): DataResult + @GET("courses/{courseId}/enrollments?state[]=current_and_concluded") fun getUserEnrollmentsForGradingPeriod(@Path("courseId") courseId: Long, @Query("user_id") userId: Long, @Query("grading_period_id") gradingPeriodId: Long): Call> + @GET("courses/{courseId}/enrollments?state[]=current_and_concluded") + suspend fun getUserEnrollmentsForGradingPeriod( + @Path("courseId") courseId: Long, + @Query("user_id") userId: Long, + @Query("grading_period_id") gradingPeriodId: Long, + @Tag params: RestParams + ): DataResult> + @GET("courses/{courseId}/rubrics/{rubricId}") fun getRubricSettings(@Path("courseId") courseId: Long, @Path("rubricId") rubricId: Long): Call @@ -275,8 +310,4 @@ object CourseAPI { fun getFirstPageCoursesWithGrades(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { callback.addCall(adapter.build(CoursesInterface::class.java, params).getFirstPageCoursesWithGrades()).enqueue(callback) } - - fun getFirstPageCoursesByEnrollmentState(enrollmentState: String, adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { - callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCoursesByEnrollmentState(enrollmentState)).enqueue(callback) - } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt index ac1db02e6a..9b592b8532 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt @@ -27,6 +27,7 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.postmodels.DiscussionEntryPostBody import com.instructure.canvasapi2.models.postmodels.DiscussionTopicPostBody import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.DataResult import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -38,7 +39,7 @@ import java.io.File object DiscussionAPI { - internal interface DiscussionInterface { + interface DiscussionInterface { @Multipart @POST("{contextType}/{contextId}/discussion_topics") @@ -78,21 +79,36 @@ object DiscussionAPI { @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long): Call> + @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") + suspend fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag params: RestParams): DataResult> + @GET("{contextType}/{contextId}/discussion_topics/{topicId}?include[]=sections") fun getDetailedDiscussion(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long): Call + @GET("{contextType}/{contextId}/discussion_topics/{topicId}?include[]=sections") + suspend fun getDetailedDiscussion(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Tag params: RestParams): DataResult + @GET("{contextType}/{contextId}/discussion_topics/{topicId}/view") fun getFullDiscussionTopic(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Query("include_new_entries") includeNewEntries: Int): Call + @GET("{contextType}/{contextId}/discussion_topics/{topicId}/view") + suspend fun getFullDiscussionTopic(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Query("include_new_entries") includeNewEntries: Int, @Tag params: RestParams): DataResult + @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/rating") fun rateDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Query("rating") rating: Int): Call + @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/rating") + suspend fun rateDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Query("rating") rating: Int, @Tag params: RestParams): DataResult + @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/read") fun markDiscussionTopicRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long): Call @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/read") fun markDiscussionTopicEntryRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long): Call + @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/read") + suspend fun markDiscussionTopicEntryRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Tag params: RestParams): DataResult + @DELETE("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/read") fun markDiscussionTopicEntryUnread(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long): Call @@ -108,6 +124,9 @@ object DiscussionAPI { @DELETE("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}") fun deleteDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") courseId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long): Call + @DELETE("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}") + suspend fun deleteDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") courseId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Tag params: RestParams): DataResult + @Multipart @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/replies") fun postDiscussionReply(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Part("message") message: RequestBody): Call @@ -130,6 +149,9 @@ object DiscussionAPI { @GET fun getNextPage(@Url nextUrl: String): Call> + @GET + suspend fun getNextPage(@Url nextUrl: String, @Tag params: RestParams): DataResult> + @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}") fun updateDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Body entry: DiscussionEntryPostBody): Call @@ -142,6 +164,9 @@ object DiscussionAPI { @GET("{contextType}/{contextId}/discussion_topics/{topicId}") fun getDiscussionTopicHeader(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long): Call + + @GET("{contextType}/{contextId}/discussion_topics/{topicId}") + suspend fun getDiscussionTopicHeader(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Tag params: RestParams): DataResult } fun createDiscussion(adapter: RestBuilder, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt index a0ebdc7dfb..b69242d366 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt @@ -22,6 +22,7 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.* @@ -36,11 +37,14 @@ object EnrollmentAPI { const val STATE_CREATION_PENDING = "creation_pending" const val STATE_CURRENT_AND_FUTURE = "current_and_future" - internal interface EnrollmentInterface { + interface EnrollmentInterface { @get:GET("users/self/enrollments?include[]=observed_users&include[]=avatar_url&state[]=creation_pending&state[]=invited&state[]=active&state[]=completed") val firstPageObserveeEnrollments: Call> + @GET("users/self/enrollments?include[]=observed_users&include[]=avatar_url&state[]=creation_pending&state[]=invited&state[]=active&state[]=completed") + suspend fun firstPageObserveeEnrollments(@Tag params: RestParams): DataResult> + @GET("courses/{courseId}/enrollments?include[]=avatar_url&state[]=active") fun getFirstPageEnrollmentsForCourse( @Path("courseId") courseId: Long, @@ -52,9 +56,19 @@ object EnrollmentAPI { @Query("userId") userId: Long, @Query("type[]") enrollmentTypes: Array): Call> + @GET("courses/{courseId}/enrollments?include[]=current_points") + suspend fun getEnrollmentsForUserInCourse( + @Path("courseId") courseId: Long, + @Query("user_id") userId: Long, + @Tag restParams: RestParams + ): DataResult> + @GET fun getNextPage(@Url nextUrl: String): Call> + @GET + suspend fun getNextPage(@Url nextUrl: String, @Tag params: RestParams): DataResult> + @GET("users/self/enrollments") fun getFirstPageSelfEnrollments( @Query("type[]") types: List?, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt index 047fdf050a..8d7297ad3e 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FeaturesAPI.kt @@ -34,6 +34,9 @@ object FeaturesAPI { @GET("courses/{courseId}/features/enabled") fun getEnabledFeaturesForCourse(@Path("courseId") contextId: Long): Call> + @GET("courses/{courseId}/features/enabled") + suspend fun getEnabledFeaturesForCourse(@Path("courseId") contextId: Long, @Tag params: RestParams): DataResult> + @GET("features/environment") fun getEnvironmentFeatureFlags(): Call> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileDownloadAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileDownloadAPI.kt index 854320dd90..0e76b81563 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileDownloadAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileDownloadAPI.kt @@ -18,11 +18,14 @@ package com.instructure.canvasapi2.apis +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.utils.DataResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import okhttp3.ResponseBody import retrofit2.http.GET import retrofit2.http.Streaming +import retrofit2.http.Tag import retrofit2.http.Url import java.io.File @@ -30,12 +33,12 @@ interface FileDownloadAPI { @Streaming @GET - suspend fun downloadFile(@Url url: String): ResponseBody + suspend fun downloadFile(@Url url: String, @Tag params: RestParams): DataResult } sealed class DownloadState { - data class InProgress(val progress: Int) : DownloadState() - object Success : DownloadState() + data class InProgress(val progress: Int, val totalBytes: Long) : DownloadState() + data class Success(val totalBytes: Long) : DownloadState() data class Failure(val throwable: Throwable) : DownloadState() } @@ -43,12 +46,14 @@ fun ResponseBody.saveFile(file: File): Flow { val debounce = 500L return flow { - emit(DownloadState.InProgress(0)) + emit(DownloadState.InProgress(0, 0)) var lastUpdate = System.currentTimeMillis() try { + var totalBytes: Long byteStream().use { inputStream -> file.outputStream().use { outputStream -> - val totalBytes = contentLength() + totalBytes = contentLength() + emit(DownloadState.InProgress(0, totalBytes)) val buffer = ByteArray(8 * 1024) var progressBytes = 0L var bytes = inputStream.read(buffer) @@ -59,13 +64,13 @@ fun ResponseBody.saveFile(file: File): Flow { bytes = inputStream.read(buffer) if (System.currentTimeMillis() - lastUpdate > debounce) { - emit(DownloadState.InProgress((progressBytes * 100 / totalBytes).toInt())) + emit(DownloadState.InProgress((progressBytes * 100 / totalBytes).toInt(), totalBytes)) lastUpdate = System.currentTimeMillis() } } } } - emit(DownloadState.Success) + emit(DownloadState.Success(totalBytes)) } catch (e: Exception) { emit(DownloadState.Failure(e)) } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt index b005003092..511e8c4759 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/FileFolderAPI.kt @@ -23,6 +23,7 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.* import java.io.IOException @@ -31,7 +32,7 @@ import java.util.* object FileFolderAPI { - internal interface FilesFoldersInterface { + interface FilesFoldersInterface { @GET("self/folders/root") fun getRootUserFolder(): Call @@ -39,27 +40,52 @@ object FileFolderAPI { @GET("{contextId}/folders/root") fun getRootFolderForContext(@Path("contextId") contextId: Long): Call + @GET("{contextType}/{contextId}/folders/root") + suspend fun getRootFolderForContext( + @Path("contextId") contextId: Long, + @Path("contextType") contextType: String, + @Tag params: RestParams + ): DataResult + @GET("folders/{folderId}") fun getFolder(@Path("folderId") folderId: Long): Call + @GET("folders/{folderId}") + suspend fun getFolder(@Path("folderId") folderId: Long, @Tag restParams: RestParams): DataResult + @GET("courses/{courseId}/files/{folderId}") fun getCourseFile(@Path("courseId") contextId: Long, @Path("folderId") folderId: Long): Call + @GET("courses/{courseId}/files/{fileId}") + suspend fun getCourseFile(@Path("courseId") contextId: Long, @Path("fileId") folderId: Long, @Tag params: RestParams): DataResult + @GET("users/self/files/{folderId}") fun getUserFile(@Path("folderId") folderId: Long): Call @GET("folders/{folderId}/folders") fun getFirstPageFolders(@Path("folderId") folderId: Long): Call> + @GET("folders/{folderId}/folders") + suspend fun getFirstPageFolders(@Path("folderId") folderId: Long, @Tag params: RestParams): DataResult> + @GET("folders/{folderId}/files?include[]=usage_rights") fun getFirstPageFiles(@Path("folderId") folderId: Long): Call> + @GET("folders/{folderId}/files?include[]=usage_rights") + suspend fun getFirstPageFiles(@Path("folderId") folderId: Long, @Tag params: RestParams): DataResult> + @GET("{fileUrl}") fun getFileFolderFromURL(@Path(value = "fileUrl", encoded = true) fileURL: String): Call + @GET("{fileUrl}") + suspend fun getFileFolderFromURL(@Path(value = "fileUrl", encoded = true) fileURL: String, @Tag params: RestParams): DataResult + @GET fun getNextPageFileFoldersList(@Url nextURL: String): Call> + @GET + suspend fun getNextPageFileFoldersList(@Url nextURL: String, @Tag params: RestParams): DataResult> + @GET("{canvasContext}/files") fun searchFiles(@Path(value = "canvasContext", encoded = true) contextPath: String, @Query("search_term") query: String): Call> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/GroupAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/GroupAPI.kt index 8ad38b8aa8..c130eff6df 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/GroupAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/GroupAPI.kt @@ -58,6 +58,9 @@ object GroupAPI { @GET("groups/{groupId}?include[]=permissions&include[]=favorites") fun getDetailedGroup(@Path("groupId") groupId: Long): Call + @GET("groups/{groupId}?include[]=permissions&include[]=favorites") + suspend fun getDetailedGroup(@Path("groupId") groupId: Long, @Tag params: RestParams): DataResult + @POST("users/self/favorites/groups/{groupId}") fun addGroupToFavorites(@Path("groupId") groupId: Long): Call @@ -72,6 +75,9 @@ object GroupAPI { @GET("groups/{groupId}/permissions") fun getGroupPermissions(@Path("groupId") groupId: Long, @Query("permissions[]") requestedPermissions: List): Call + + @GET("groups/{groupId}/permissions") + suspend fun getGroupPermissions(@Path("groupId") groupId: Long, @Query("permissions[]") requestedPermissions: List, @Tag params: RestParams): DataResult } fun getFirstPageGroups(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt index 1b20b209b4..3f331637d2 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ModuleAPI.kt @@ -20,36 +20,52 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.* -internal object ModuleAPI { +object ModuleAPI { - internal interface ModuleInterface { + interface ModuleInterface { @GET("{contextId}/modules") fun getFirstPageModuleObjects(@Path("contextId") contextId: Long) : Call> + @GET("{contextType}/{contextId}/modules") + suspend fun getFirstPageModuleObjects(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag params: RestParams) : DataResult> + @GET fun getNextPageModuleObjectList(@Url nextURL: String) : Call> + @GET + suspend fun getNextPageModuleObjectList(@Url nextURL: String, @Tag params: RestParams) : DataResult> + @GET("{contextId}/modules?include[]=items&include[]=content_details") fun getFirstPageModulesWithItems(@Path("contextId") contextId: Long) : Call> @GET("{contextId}/modules/{moduleId}/items?include[]=content_details&include[]=mastery_paths") fun getFirstPageModuleItems(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long) : Call> + @GET("{contextType}/{contextId}/modules/{moduleId}/items?include[]=content_details&include[]=mastery_paths") + suspend fun getFirstPageModuleItems(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Tag params: RestParams) : DataResult> + @GET fun getNextPageModuleItemList(@Url nextURL: String) : Call> + @GET + suspend fun getNextPageModuleItemList(@Url nextURL: String, @Tag params: RestParams) : DataResult> + @POST("{contextId}/modules/{moduleId}/items/{itemId}/mark_read") fun markModuleItemRead(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long) : Call - @PUT("{contextId}/modules/{moduleId}/items/{itemId}/done") - fun markModuleAsDone(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long) : Call + @POST("{contextType}/{contextId}/modules/{moduleId}/items/{itemId}/mark_read") + suspend fun markModuleItemRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long, @Tag params: RestParams) : DataResult - @DELETE("{contextId}/modules/{moduleId}/items/{itemId}/done") - fun markModuleAsNotDone(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long): Call + @PUT("{contextType}/{contextId}/modules/{moduleId}/items/{itemId}/done") + suspend fun markModuleItemAsDone(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long, @Tag params: RestParams) : DataResult + + @DELETE("{contextType}/{contextId}/modules/{moduleId}/items/{itemId}/done") + suspend fun markModuleItemAsNotDone(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long, @Tag params: RestParams): DataResult @POST("{contextId}/modules/{moduleId}/items/{itemId}/select_mastery_path") fun selectMasteryPath(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long, @Query("assignment_set_id") assignmentSetId: Long) : Call @@ -57,6 +73,9 @@ internal object ModuleAPI { @GET("{contextId}/module_item_sequence") fun getModuleItemSequence(@Path("contextId") contextId: Long, @Query("asset_type") assetType: String, @Query("asset_id") assetId: String) : Call + @GET("{contextType}/{contextId}/module_item_sequence") + suspend fun getModuleItemSequence(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Query("asset_type") assetType: String, @Query("asset_id") assetId: String, @Tag params: RestParams) : DataResult + @GET("{contextId}/modules/{moduleId}/items/{itemId}?include[]=content_details") fun getModuleItem(@Path("contextId") contextId: Long, @Path("moduleId") moduleId: Long, @Path("itemId") itemId: Long) : Call } @@ -109,14 +128,6 @@ internal object ModuleAPI { callback.addCall(adapter.build(ModuleInterface::class.java, params).markModuleItemRead(canvasContext.id, moduleId, itemId)).enqueue(callback) } - fun markModuleAsDone(adapter: RestBuilder, params: RestParams,canvasContext: CanvasContext, moduleId: Long, itemId: Long, callback: StatusCallback) { - callback.addCall(adapter.build(ModuleInterface::class.java, params).markModuleAsDone(canvasContext.id, moduleId, itemId)).enqueue(callback) - } - - fun markModuleAsNotDone(adapter: RestBuilder, params: RestParams,canvasContext: CanvasContext, moduleId: Long, itemId: Long, callback: StatusCallback) { - callback.addCall(adapter.build(ModuleInterface::class.java, params).markModuleAsNotDone(canvasContext.id, moduleId, itemId)).enqueue(callback) - } - fun selectMasteryPath(adapter: RestBuilder, params: RestParams,canvasContext: CanvasContext, moduleId: Long, itemId: Long, assignmentSetId: Long, callback: StatusCallback) { callback.addCall(adapter.build(ModuleInterface::class.java, params).selectMasteryPath(canvasContext.id, moduleId, itemId, assignmentSetId)).enqueue(callback) } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/OAuthAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/OAuthAPI.kt index fd754714ab..6a24e31fc7 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/OAuthAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/OAuthAPI.kt @@ -31,7 +31,7 @@ import java.io.IOException object OAuthAPI { - internal interface OAuthInterface { + interface OAuthInterface { @DELETE("/login/oauth2/token") fun deleteToken(): Call @@ -47,6 +47,9 @@ object OAuthAPI { @GET("/login/session_token") fun getAuthenticatedSession(@Query("return_to") targetUrl: String): Call + @GET("/login/session_token") + suspend fun getAuthenticatedSession(@Query("return_to") targetUrl: String, @Tag params: RestParams): DataResult + @GET("/api/v1/login/session_token") fun getAuthenticatedSessionMasquerading(@Query("return_to") targetUrl: String, @Query("as_user_id") userId: Long): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PageAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PageAPI.kt index 98b72141ab..19a97b7838 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PageAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/PageAPI.kt @@ -24,30 +24,67 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Page import com.instructure.canvasapi2.models.postmodels.PagePostBodyWrapper import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.DataResult import okhttp3.RequestBody import retrofit2.Call import retrofit2.http.* object PageAPI { - internal interface PagesInterface { + interface PagesInterface { @GET("{contextId}/pages?sort=title&order=asc") fun getFirstPagePages( @Path("contextId") contextId: Long): Call> + @GET("{contextType}/{contextId}/pages?sort=title&order=asc") + suspend fun getFirstPagePages( + @Path("contextId") contextId: Long, @Path("contextType") contextType: String, @Tag params: RestParams + ): DataResult> + + @GET("{contextType}/{contextId}/pages?sort=title&order=asc&include[]=body") + suspend fun getFirstPagePagesWithBody( + @Path("contextId") contextId: Long, @Path("contextType") contextType: String, @Tag params: RestParams + ): DataResult> + @GET fun getNextPagePagesList( - @Url nextURL: String): Call> + @Url nextURL: String + ): Call> + + @GET + suspend fun getNextPagePagesList( + @Url nextURL: String, @Tag params: RestParams + ): DataResult> @GET("{contextId}/pages/{pageId}") fun getDetailedPage( @Path("contextId") contextId: Long, @Path("pageId") pageId: String): Call + @GET("courses/{contextId}/pages/{pageId}") + suspend fun getDetailedPage(@Path("contextId") contextId: Long, + @Path("pageId") pageId: String, + @Tag params: RestParams): DataResult + + @GET("{contextType}/{contextId}/pages/{pageId}") + suspend fun getDetailedPage( + @Path("contextType") contextType: String, + @Path("contextId") contextId: Long, + @Path("pageId") pageId: String, + @Tag params: RestParams + ): DataResult + @GET("{contextId}/front_page") fun getFrontPage( @Path("contextId") contextId: Long): Call + @GET("{contextType}/{contextId}/front_page") + suspend fun getFrontPage( + @Path("contextType") contextType: String, + @Path("contextId") contextId: Long, + @Tag params: RestParams + ): DataResult + @PUT("{contextId}/pages/{pageUrl}") fun editPage( @Path("contextId") contextId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/QuizAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/QuizAPI.kt index 5d525a8cb0..0700b789a4 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/QuizAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/QuizAPI.kt @@ -22,26 +22,35 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.models.postmodels.QuizPostBodyWrapper -import okhttp3.ResponseBody +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.* import java.util.* object QuizAPI { - internal interface QuizInterface { + interface QuizInterface { @GET("{contextType}/{contextId}/all_quizzes") fun getFirstPageQuizzesList(@Path("contextType") contextType: String, @Path("contextId") contextId: Long): Call> + @GET("{contextType}/{contextId}/all_quizzes") + suspend fun getFirstPageQuizzesList(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag restParams: RestParams): DataResult> + @GET fun getNextPageQuizzesList(@Url nextURL: String): Call> + @GET + suspend fun getNextPageQuizzesList(@Url nextURL: String, @Tag restParams: RestParams): DataResult> + @GET("{contextType}/{contextId}/quizzes/{quizId}") fun getDetailedQuiz(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("quizId") quizId: Long): Call @GET fun getDetailedQuizByUrl(@Url quizUrl: String): Call + @GET + suspend fun getDetailedQuizByUrl(@Url quizUrl: String, @Tag params: RestParams): DataResult + @GET("courses/{courseId}/all_quizzes") fun getFirstPageQuizzes(@Path("courseId") contextId: Long): Call> @@ -51,6 +60,9 @@ object QuizAPI { @GET("courses/{courseId}/quizzes/{quizId}") fun getQuiz(@Path("courseId") courseId: Long, @Path("quizId") quizId: Long): Call + @GET("courses/{courseId}/quizzes/{quizId}") + suspend fun getQuiz(@Path("courseId") courseId: Long, @Path("quizId") quizId: Long, @Tag restParams: RestParams): DataResult + @PUT("courses/{courseId}/quizzes/{quizId}") fun editQuiz( @Path("courseId") courseId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt index 7902a4c3ce..cce8bf26fe 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/SubmissionAPI.kt @@ -21,6 +21,7 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.* @@ -32,7 +33,7 @@ object SubmissionAPI { private const val pointsPostFix = "][points]" private const val commentsPostFix = "][comments]" - internal interface SubmissionInterface { + interface SubmissionInterface { @GET("courses/{courseId}/assignments/{assignmentId}/submissions/{studentId}?include[]=rubric_assessment&include[]=submission_history&include[]=submission_comments&include[]=group") fun getSingleSubmission( @@ -40,15 +41,34 @@ object SubmissionAPI { @Path("assignmentId") assignmentId: Long, @Path("studentId") studentId: Long): Call + @GET("courses/{courseId}/assignments/{assignmentId}/submissions/{studentId}?include[]=rubric_assessment&include[]=submission_history&include[]=submission_comments&include[]=group") + suspend fun getSingleSubmission( + @Path("courseId") courseId: Long, + @Path("assignmentId") assignmentId: Long, + @Path("studentId") studentId: Long, + @Tag restParams: RestParams + ): DataResult + @GET("courses/{courseId}/students/submissions?include[]=assignment&include[]=rubric_assessment&include[]=submission_history&include[]=submission_comments&include[]=group") fun getSubmissionsForMultipleAssignments( @Path("courseId") courseId: Long, @Query("student_ids[]") studentId: Long, @Query("assignment_ids[]") assignmentIds: List): Call> + @GET("courses/{courseId}/students/submissions?include[]=assignment&include[]=rubric_assessment&include[]=submission_history&include[]=submission_comments&include[]=group") + suspend fun getSubmissionsForMultipleAssignments( + @Path("courseId") courseId: Long, + @Query("student_ids[]") studentId: Long, + @Query("assignment_ids[]") assignmentIds: List, + @Tag restParams: RestParams + ): DataResult> + @GET fun getNextPageSubmissions(@Url nextUrl: String): Call> + @GET + suspend fun getNextPageSubmissions(@Url nextUrl: String, @Tag restParams: RestParams): DataResult> + @PUT("courses/{courseId}/assignments/{assignmentId}/submissions/{userId}") fun postSubmissionRubricAssessmentMap( @Path("courseId") courseId: Long, @@ -129,6 +149,9 @@ object SubmissionAPI { @GET fun getLtiFromAuthenticationUrl(@Url url: String): Call + @GET + suspend fun getLtiFromAuthenticationUrl(@Url url: String, @Tag restParams: RestParams): DataResult + @PUT("courses/{contextId}/assignments/{assignmentId}/submissions/{userId}") fun postSubmissionGrade(@Path("contextId") contextId: Long, @Path("assignmentId") assignmentId: Long, @Path("userId") userId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt index 0260f2e6c7..d7cc7b1ed9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/TabAPI.kt @@ -19,17 +19,22 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Tab +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Tag object TabAPI { - internal interface TabsInterface { + interface TabsInterface { @GET("{contextId}/tabs") fun getTabs(@Path("contextId") contextId: Long): Call> + @GET("{contextType}/{contextId}/tabs") + suspend fun getTabs(@Path("contextId") contextId: Long, @Path("contextType") contextType: String, @Tag params: RestParams): DataResult> + @GET("{contextId}/tabs?include[]=course_subject_tabs") fun getTabsForElementary(@Path("contextId") contextId: Long): Call> } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt index 8261410f91..f1b0dea912 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt @@ -22,7 +22,7 @@ import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.* import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.* @@ -33,7 +33,7 @@ object UserAPI { STUDENT, TEACHER, TA, OBSERVER, DESIGNER } - internal interface UsersInterface { + interface UsersInterface { @GET("users/self/colors") fun getColors(): Call @@ -41,6 +41,9 @@ object UserAPI { @GET("users/self/profile") fun getSelf(): Call + @GET("users/self/profile") + suspend fun getSelf(@Tag params: RestParams): DataResult + @GET("users/self/settings") fun getSelfSettings(): Call @@ -74,18 +77,32 @@ object UserAPI { @GET("users/{userId}/profile") fun getUser(@Path("userId") userId: Long?): Call + @GET("users/{userId}/profile") + suspend fun getUser(@Path("userId") userId: Long, @Tag params: RestParams): DataResult + @GET("{contextType}/{contextId}/users/{userId}?include[]=avatar_url&include[]=user_id&include[]=email&include[]=bio&include[]=enrollments") fun getUserForContextId(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("userId") userId: Long): Call + @GET("{contextType}/{contextId}/users/{userId}?include[]=avatar_url&include[]=user_id&include[]=email&include[]=bio&include[]=enrollments") + suspend fun getUserForContextId(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("userId") userId: Long, @Tag params: RestParams): DataResult + @GET("{context_id}/users?include[]=enrollments&include[]=avatar_url&include[]=user_id&include[]=email&include[]=bio&exclude_inactive=true") fun getFirstPagePeopleList(@Path("context_id") context_id: Long, @Query("enrollment_type") enrollmentType: String): Call> + @GET("{contextType}/{context_id}/users?include[]=enrollments&include[]=avatar_url&include[]=user_id&include[]=email&include[]=bio&exclude_inactive=true") + suspend fun getFirstPagePeopleList(@Path("context_id") context_id: Long, @Path("contextType") contextType: String, @Tag params: RestParams, @Query("enrollment_type") enrollmentType: String? = null): DataResult> + @GET("{context_id}/users?include[]=enrollments&include[]=avatar_url&include[]=user_id&include[]=email&include[]=bio&exclude_inactive=true") fun getFirstPageAllPeopleList(@Path("context_id") context_id: Long): Call> @GET fun next(@Url nextURL: String): Call> + @GET + suspend fun getNextPagePeopleList( + @Url nextURL: String, @Tag params: RestParams + ): DataResult> + @GET("accounts/{accountId}/permissions?permissions[]=become_user") fun getBecomeUserPermission(@Path("accountId") accountId: Long): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCall.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCall.kt index 07412577ec..90d1304bc9 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCall.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCall.kt @@ -25,10 +25,11 @@ import okio.Timeout import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import java.lang.reflect.Type -class DataResultCall(private val delegate: Call): Call> { +class DataResultCall(private val delegate: Call, private val successType: Type): Call> { - override fun clone(): Call> = DataResultCall(delegate.clone()) + override fun clone(): Call> = DataResultCall(delegate.clone(), successType) override fun execute(): Response> { throw UnsupportedOperationException("DataResultCall doesn't support execute") @@ -44,12 +45,12 @@ class DataResultCall(private val delegate: Call): Call callback.onResponse(this@DataResultCall, Response.success(createSuccessResult(response))) } else { if (error != null) { - val failure = if (code == 401) { + val failure = if (code == 401 || code == 403) { Failure.Authorization(response.message()) } else { - Failure.Network(response.message()) + Failure.Network(response.message(), code) } - callback.onResponse(this@DataResultCall, Response.success(DataResult.Fail(failure))) + callback.onResponse(this@DataResultCall, Response.success(DataResult.Fail(failure, response.raw()))) } else { callback.onResponse(this@DataResultCall, Response.success(DataResult.Fail())) } @@ -64,13 +65,25 @@ class DataResultCall(private val delegate: Call): Call private fun createSuccessResult(response: Response): DataResult { val body = response.body() - return if (body != null) { + val unitResponse = successType.typeName == Unit.javaClass.name + + return if (body != null || unitResponse) { val linkHeaders = APIHelper.parseLinkHeaderResponse(response.headers()) val isCachedResponse = APIHelper.isCachedResponse(response.raw()) val apiType = if (isCachedResponse) ApiType.CACHE else ApiType.API - DataResult.Success(body, linkHeaders, apiType) + + if (body == null) { + try { + // This should always be Unit, but we can catch the exception and return a Failure if it's not. (In cases where the API interface is misconfigured) + DataResult.Success(Unit as T, linkHeaders, apiType) + } catch (e: ClassCastException) { + return DataResult.Fail(Failure.ParsingError) + } + } else { + DataResult.Success(body, linkHeaders, apiType) + } } else { - DataResult.Fail() + DataResult.Fail(Failure.ParsingError) } } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCallAdapter.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCallAdapter.kt index 4e82ffcf38..2c7912bab5 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCallAdapter.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/calladapter/DataResultCallAdapter.kt @@ -28,6 +28,6 @@ class DataResultCallAdapter( override fun responseType(): Type = successType override fun adapt(call: Call): Call> { - return DataResultCall(call) + return DataResultCall(call, successType) } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt index 1937796805..92e7a1c7e0 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt @@ -1,14 +1,6 @@ package com.instructure.canvasapi2.di -import com.instructure.canvasapi2.apis.CourseAPI -import com.instructure.canvasapi2.apis.FeaturesAPI -import com.instructure.canvasapi2.apis.FileDownloadAPI -import com.instructure.canvasapi2.apis.GroupAPI -import com.instructure.canvasapi2.apis.HelpLinksAPI -import com.instructure.canvasapi2.apis.InboxApi -import com.instructure.canvasapi2.apis.NotificationPreferencesAPI -import com.instructure.canvasapi2.apis.PlannerAPI -import com.instructure.canvasapi2.apis.ProgressAPI +import com.instructure.canvasapi2.apis.* import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.managers.* @@ -171,6 +163,76 @@ class ApiModule { return RestBuilder().build(ProgressAPI.ProgressInterface::class.java, RestParams()) } + @Provides + fun provideTabApi(): TabAPI.TabsInterface { + return RestBuilder().build(TabAPI.TabsInterface::class.java, RestParams()) + } + + @Provides + fun providesUserApi(): UserAPI.UsersInterface { + return RestBuilder().build(UserAPI.UsersInterface::class.java, RestParams()) + } + + @Provides + fun providePageApi(): PageAPI.PagesInterface { + return RestBuilder().build(PageAPI.PagesInterface::class.java, RestParams()) + } + + @Provides + fun provideAssignmentApi(): AssignmentAPI.AssignmentInterface { + return RestBuilder().build(AssignmentAPI.AssignmentInterface::class.java, RestParams()) + } + + @Provides + fun provideFileFolderApi(): FileFolderAPI.FilesFoldersInterface { + return RestBuilder().build(FileFolderAPI.FilesFoldersInterface::class.java, RestParams()) + } + + @Provides + fun provideQuizApi(): QuizAPI.QuizInterface { + return RestBuilder().build(QuizAPI.QuizInterface::class.java, RestParams()) + } + + @Provides + fun provideSubmissionApi(): SubmissionAPI.SubmissionInterface { + return RestBuilder().build(SubmissionAPI.SubmissionInterface::class.java, RestParams()) + } + + @Provides + fun provideCalendarEventApi(): CalendarEventAPI.CalendarEventInterface { + return RestBuilder().build(CalendarEventAPI.CalendarEventInterface::class.java, RestParams()) + } + + @Provides + fun provideEnrollmentApi(): EnrollmentAPI.EnrollmentInterface { + return RestBuilder().build(EnrollmentAPI.EnrollmentInterface::class.java, RestParams()) + } + + @Provides + fun providesConferencesApi(): ConferencesApi.ConferencesInterface { + return RestBuilder().build(ConferencesApi.ConferencesInterface::class.java, RestParams()) + } + + @Provides + fun providesOAuthApi(): OAuthAPI.OAuthInterface { + return RestBuilder().build(OAuthAPI.OAuthInterface::class.java, RestParams()) + } + + @Provides + fun provideDiscussionApi(): DiscussionAPI.DiscussionInterface { + return RestBuilder().build(DiscussionAPI.DiscussionInterface::class.java, RestParams()) + } + + @Provides + fun provideAnnouncementApi(): AnnouncementAPI.AnnouncementInterface { + return RestBuilder().build(AnnouncementAPI.AnnouncementInterface::class.java, RestParams()) + } + + @Provides + fun provideModuleApi(): ModuleAPI.ModuleInterface { + return RestBuilder().build(ModuleAPI.ModuleInterface::class.java, RestParams()) + } + @Provides fun provideFeaturesApi(): FeaturesAPI.FeaturesInterface { return RestBuilder().build(FeaturesAPI.FeaturesInterface::class.java, RestParams()) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt index ea621abb56..cf666cc10a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/CourseManager.kt @@ -107,23 +107,6 @@ object CourseManager { CourseAPI.getFirstPageCoursesWithConcluded(adapter, depaginatedCallback, params) } - fun getCoursesByEnrollmentState(enrollmentState: String, forceNetwork: Boolean, callback: StatusCallback>) { - - val adapter = RestBuilder(callback) - val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) - - val depaginatedCallback = object : ExhaustiveListCallback(callback) { - override fun getNextPage(callback: StatusCallback>, nextUrl: String, isCached: Boolean) { - CourseAPI.getNextPageCourses(forceNetwork, nextUrl, adapter, callback) - } - } - - adapter.statusCallback = depaginatedCallback - CourseAPI.getFirstPageCoursesByEnrollmentState(enrollmentState, adapter, depaginatedCallback, params) - } - - fun getCoursesByEnrollmentStateAsync(enrollmentState: String, forceNetwork: Boolean) = apiAsync> { getCoursesByEnrollmentState(enrollmentState, forceNetwork, it) } - fun getCoursesWithConcludedAsync(forceNetwork: Boolean) = apiAsync> { getCoursesWithConcluded(forceNetwork, it) } fun getDashboardCoursesAsync(forceNetwork: Boolean) = apiAsync> { getDashboardCourses(forceNetwork, it) } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ModuleManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ModuleManager.kt index d06cbe9688..2f7788897f 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ModuleManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ModuleManager.kt @@ -19,7 +19,11 @@ import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.apis.ModuleAPI import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.builders.RestParams -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.MasteryPathSelectResponse +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleItemSequence +import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.utils.ExhaustiveListCallback import com.instructure.canvasapi2.utils.weave.apiAsync import okhttp3.ResponseBody @@ -126,23 +130,6 @@ object ModuleManager { ModuleAPI.getAllModuleItems(adapter, params, canvasContext.id, moduleId, depaginatedCallback) } - fun markAsDone(canvasContext: CanvasContext, moduleId: Long, itemId: Long, callback: StatusCallback) { - val adapter = RestBuilder(callback) - val params = RestParams(canvasContext = canvasContext) - ModuleAPI.markModuleAsDone(adapter, params, canvasContext, moduleId, itemId, callback) - } - - fun markAsNotDone( - canvasContext: CanvasContext, - moduleId: Long, - itemId: Long, - callback: StatusCallback - ) { - val adapter = RestBuilder(callback) - val params = RestParams(canvasContext = canvasContext) - ModuleAPI.markModuleAsNotDone(adapter, params, canvasContext, moduleId, itemId, callback) - } - fun markModuleItemAsRead( canvasContext: CanvasContext, moduleId: Long, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt index 7ac80447ba..4fa9e37b98 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Assignment.kt @@ -29,7 +29,7 @@ import java.util.* data class Assignment( override var id: Long = 0, var name: String? = null, - val description: String? = null, + var description: String? = null, @SerializedName("submission_types") val submissionTypesRaw: List = arrayListOf(), @SerializedName("due_at") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AssignmentScoreStatistics.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AssignmentScoreStatistics.kt index 9c947ad279..17e2795814 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AssignmentScoreStatistics.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AssignmentScoreStatistics.kt @@ -1,12 +1,7 @@ package com.instructure.canvasapi2.models -import android.content.Context import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import com.instructure.canvasapi2.R -import com.instructure.canvasapi2.utils.toDate import kotlinx.parcelize.Parcelize -import java.util.Date @Parcelize data class AssignmentScoreStatistics( diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt index 13a694a693..d23360c3ff 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Course.kt @@ -80,6 +80,8 @@ data class Course( val courseColor: String? = null, @SerializedName("grading_periods") val gradingPeriods: List? = null, + @SerializedName("tabs") + val tabs: List? = null, @SerializedName("settings") val settings: CourseSettings? = null, @SerializedName("grading_scheme") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DashboardCard.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DashboardCard.kt index aa72b3a42a..72aa766645 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DashboardCard.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DashboardCard.kt @@ -24,5 +24,6 @@ data class DashboardCard( val isK5Subject: Boolean = false, val shortName: String? = null, val originalName: String? = null, - val courseCode: String? = null + val courseCode: String? = null, + val position: Int = Int.MAX_VALUE // We should always get back the position, but in case it's missing we just put it at the end ) : CanvasModel() \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DiscussionEntry.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DiscussionEntry.kt index 62bb0b845f..61fd3011e3 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DiscussionEntry.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/DiscussionEntry.kt @@ -73,12 +73,12 @@ data class DiscussionEntry( return depth } - fun init(topic: DiscussionTopic, parentEntry: DiscussionEntry) { + fun init(topic: DiscussionTopic, parentEntry: DiscussionEntry, isOnline: Boolean = true) { parent = parentEntry // The server attaches a verifier param on the end of img src urls inside of a discussion, however // this happens whenever the server decides to make it happen so we need to make sure that the image // contains this param in it's url, or replace it with an authenticated url so we can download it for all to see - if (parentEntry.message?.contains("? = null, // Comes back from the server @SerializedName("anonymous_state") - var anonymousState: String? = null + var anonymousState: String? = null, ) : CanvasModel() { override val comparisonDate: Date? get() = if (lastReplyDate != null) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/FileFolder.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/FileFolder.kt index d2abe85a03..d520c2ebb5 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/FileFolder.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/FileFolder.kt @@ -17,8 +17,10 @@ package com.instructure.canvasapi2.models +import android.webkit.URLUtil import com.google.gson.annotations.SerializedName import com.instructure.canvasapi2.utils.NaturalOrderComparator +import com.instructure.canvasapi2.utils.isValid import kotlinx.parcelize.Parcelize import java.util.Date import java.util.Locale @@ -109,4 +111,6 @@ data class FileFolder( } } + val isLocalFile = url.isValid() && !URLUtil.isNetworkUrl(url) + } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleCompletionRequirement.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleCompletionRequirement.kt index 398e53d1b1..32ae6bd0d7 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleCompletionRequirement.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleCompletionRequirement.kt @@ -27,5 +27,6 @@ data class ModuleCompletionRequirement( @SerializedName("min_score") val minScore: Double = 0.0, @SerializedName("max_score") - val maxScore: Double = 0.0 + val maxScore: Double = 0.0, + var completed: Boolean = false ) : CanvasModel() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt index 22a6b5d82e..a6da57f5d1 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ModuleItem.kt @@ -34,7 +34,7 @@ data class ModuleItem( val htmlUrl: String? = null, val url: String? = null, @SerializedName("completion_requirement") - val completionRequirement: CompletionRequirement? = null, + val completionRequirement: ModuleCompletionRequirement? = null, @SerializedName("content_details") val moduleDetails: ModuleContentDetails? = null, val published: Boolean? = null, @@ -62,11 +62,3 @@ data class ModuleItem( } } -@Parcelize -data class CompletionRequirement( - val type: String? = null, - @SerializedName("min_score") - val minScore: Double = 0.0, - var completed: Boolean = false -) : Parcelable - diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Quiz.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Quiz.kt index 53d4a1f3c5..93193adbf6 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Quiz.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Quiz.kt @@ -33,7 +33,7 @@ data class Quiz( val mobileUrl: String? = null, @SerializedName("html_url") val htmlUrl: String? = null, - val description: String? = "", + var description: String? = "", @SerializedName("quiz_type") val quizType: String? = null, @SerializedName("assignment_group_id") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ScheduleItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ScheduleItem.kt index b63a6ee457..375a017e76 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ScheduleItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ScheduleItem.kt @@ -32,7 +32,7 @@ data class ScheduleItem( @SerializedName("id") var itemId: String = "", // Can be different values - check the getId() override method below val title: String? = null, - val description: String? = null, + var description: String? = null, @SerializedName("start_at") val startAt: String? = null, @SerializedName("end_at") diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Submission.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Submission.kt index e3a6966e2d..f861847f26 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Submission.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Submission.kt @@ -19,9 +19,7 @@ package com.instructure.canvasapi2.models import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize -import java.util.ArrayList -import java.util.Date -import java.util.HashMap +import java.util.* @JvmSuppressWildcards @Parcelize @@ -82,7 +80,9 @@ data class Submission( @SerializedName("entered_grade") val enteredGrade: String? = null, @SerializedName("posted_at") - val postedAt: Date? = null + val postedAt: Date? = null, + @SerializedName("grading_period_id") + val gradingPeriodId: Long? = null ) : CanvasModel() { override val comparisonDate get() = submittedAt override val comparisonString get() = submissionType diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Tab.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Tab.kt index da6892d481..0c597faf4d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Tab.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Tab.kt @@ -36,7 +36,8 @@ data class Tab( val isHidden: Boolean = false, // only included when true val position: Int = 0, @SerializedName("url") - val ltiUrl: String = "" + val ltiUrl: String = "", + val enabled: Boolean = true // Helper variable, not included in API response ) : CanvasModel() { override val id get() = position.toLong() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt index c755d5213e..a1333f4627 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Term.kt @@ -26,7 +26,7 @@ data class Term( override val id: Long = 0, val name: String? = null, @SerializedName("start_at") - private val startAt: String? = null, + val startAt: String? = null, @SerializedName("end_at") val endAt: String? = null, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt index a46677e4ff..c70ea627ce 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/User.kt @@ -41,7 +41,7 @@ data class User( // Helper variable for the "specified" enrollment. val enrollmentIndex: Int = 0, @SerializedName("last_login") - private val lastLogin: String? = null, + val lastLogin: String? = null, val locale: String? = null, @SerializedName("effective_locale") val effective_locale: String? = null, diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt index 2282706bbd..33d12155f4 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiPrefs.kt @@ -139,6 +139,8 @@ object ApiPrefs : PrefManager(PREFERENCE_FILE_NAME) { // Switch that lets the user manually override and turn off the elementary view in the settings var elementaryDashboardEnabledOverride by BooleanPref(true) + var checkTokenAfterOfflineLogin by BooleanPref() + val showElementaryView get() = canvasForElementary && elementaryDashboardEnabledOverride diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiType.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiType.kt index 78e0a96ff2..be0d460bd2 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiType.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ApiType.kt @@ -19,7 +19,7 @@ package com.instructure.canvasapi2.utils enum class ApiType { - API, CACHE, UNKNOWN; + API, CACHE, DB, UNKNOWN; val isAPI: Boolean get() = this == API val isCache: Boolean get() = this == CACHE diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DataResult.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DataResult.kt index 6f153035b3..f06f3005e7 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DataResult.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/DataResult.kt @@ -16,6 +16,7 @@ */ package com.instructure.canvasapi2.utils +import okhttp3.Response import retrofit2.Call sealed class DataResult { @@ -24,7 +25,8 @@ sealed class DataResult { data class Success(val data: A, val linkHeaders: LinkHeaders = LinkHeaders(), val apiType: ApiType = ApiType.UNKNOWN) : DataResult() data class Fail( - val failure: Failure? = null + val failure: Failure? = null, + val response: Response? = null, ) : DataResult() val isSuccess get() = this is Success @@ -55,9 +57,7 @@ sealed class DataResult { fun map(block: (A) -> B): DataResult { return when (this) { - is Success -> Success( - block(data) - ) + is Success -> Success(block(data), linkHeaders, apiType) is Fail -> this } } @@ -73,9 +73,10 @@ sealed class DataResult { // Simple abstraction for repository layer errors, add to as needed sealed class Failure(open val message: String?) { - data class Network(override val message: String? = null) : Failure(message) // Covers 404/500, no internet, etc. Generic case for failed request + data class Network(override val message: String? = null, val errorCode: Int? = null) : Failure(message) // Covers 404/500, no internet, etc. Generic case for failed request data class Authorization(override val message: String? = null) : Failure(message) // Covers 401, or permission errors. data class Exception(val exception: Throwable, override val message: String? = null) : Failure(message) + object ParsingError : Failure("Response was successful, but couldn't be converted to the expected type") } internal fun Call.dataResult(): DataResult { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/weave/TryWeave.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/weave/TryWeave.kt index 350ef209db..2e7bf5e23a 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/weave/TryWeave.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/weave/TryWeave.kt @@ -17,10 +17,7 @@ package com.instructure.canvasapi2.utils.weave -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* /** * Holds the data necessary for [tryWeave] to work correctly @@ -46,3 +43,15 @@ infix fun TryWeave.catch(onException: (e: Throwable) -> Unit): WeaveCoroutine { coroutine.start(CoroutineStart.DEFAULT, coroutine, block) return coroutine } + +class TryLaunch(val coroutineScope: CoroutineScope, val block: suspend CoroutineScope.() -> Unit) + +fun CoroutineScope.tryLaunch(block: suspend CoroutineScope.() -> Unit) = TryLaunch(this, block) + +infix fun TryLaunch.catch(onException: (e: Throwable) -> Unit): Job { + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + if (throwable !is CancellationException) onException(throwable) + } + + return coroutineScope.launch(context = coroutineScope.coroutineContext + exceptionHandler, block = block) +} diff --git a/libs/flutter_student_embed/lib/screens/calendar/planner_fetcher.dart b/libs/flutter_student_embed/lib/screens/calendar/planner_fetcher.dart index 0ad6d1afe6..7d3d4c1ff6 100644 --- a/libs/flutter_student_embed/lib/screens/calendar/planner_fetcher.dart +++ b/libs/flutter_student_embed/lib/screens/calendar/planner_fetcher.dart @@ -131,7 +131,7 @@ class PlannerFetcher extends ChangeNotifier { items.forEach((item) { if (item.plannableDate != null) { String dayKey = dayKeyForDate(item.plannableDate.toLocal()); - dayItems[dayKey].add(item); + dayItems[dayKey]?.add(item); } }); diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt index d236a8a53b..f8ffb23c92 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/activities/BaseLoginLandingPageActivity.kt @@ -63,7 +63,6 @@ import com.instructure.loginapi.login.util.LoginPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.loginapi.login.util.SavedLoginInfo import com.instructure.loginapi.login.viewmodel.LoginViewModel -import com.instructure.pandautils.binding.setTint import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.utils.* import java.util.* @@ -127,7 +126,7 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi if(loginWithQRCodeEnabled()) { qrLogin.setVisible() qrDivider.setVisible() - qrLogin.onClick { + qrLogin.onClickWithRequireNetwork { Analytics.logEvent(AnalyticsEventConstants.QR_CODE_LOGIN_CLICKED) startActivity(loginWithQRIntent()) } @@ -177,7 +176,7 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi } override fun onRemovePreviousUserClick(user: SignedInUser) { - PreviousUsersUtils.remove(this@BaseLoginLandingPageActivity, user) + removePreviousUser(user) } override fun onNowEmpty() { @@ -198,6 +197,10 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi } + open fun removePreviousUser(user: SignedInUser) { + PreviousUsersUtils.remove(this@BaseLoginLandingPageActivity, user) + } + private fun resizePreviousUsersRecyclerView(previousUsers: ArrayList) = with(binding) { val maxUsersToShow = resources.getInteger(R.integer.login_previousMaxVisible) if (previousUsers.size == 1 && maxUsersToShow > 1) { diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt index 7c45825960..b59928dcd8 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/tasks/LogoutTask.kt @@ -17,14 +17,12 @@ package com.instructure.loginapi.login.tasks import android.app.Activity -import android.app.NotificationManager import android.content.Context import android.content.Intent import android.net.Uri import com.instructure.canvasapi2.CanvasRestAdapter import com.instructure.canvasapi2.builders.RestBuilder import com.instructure.canvasapi2.managers.CommunicationChannelsManager -import com.instructure.canvasapi2.managers.FeaturesManager import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.canvasapi2.utils.* import com.instructure.canvasapi2.utils.weave.weave @@ -39,8 +37,9 @@ import java.io.File abstract class LogoutTask( val type: Type, val uri: Uri? = null, - val canvasForElementaryFeatureFlag: Boolean = false, - private val typefaceBehavior: TypefaceBehavior? = null) { + private val canvasForElementaryFeatureFlag: Boolean = false, + private val typefaceBehavior: TypefaceBehavior? = null +) { enum class Type { SWITCH_USERS, @@ -53,6 +52,7 @@ abstract class LogoutTask( protected abstract fun createLoginIntent(context: Context): Intent protected abstract fun createQRLoginIntent(context: Context, uri: Uri): Intent? protected abstract fun getFcmToken(listener: (registrationId: String?) -> Unit) + protected abstract fun removeOfflineData(userId: Long?) @Suppress("EXPERIMENTAL_FEATURE_WARNING") fun execute() { @@ -79,7 +79,11 @@ abstract class LogoutTask( PushNotification.clearPushHistory() when (type) { - Type.LOGOUT, Type.LOGOUT_NO_LOGIN_FLOW -> removeUser() + Type.LOGOUT, Type.LOGOUT_NO_LOGIN_FLOW -> { + removeOfflineData(ApiPrefs.user?.id) + removeUser() + } + Type.SWITCH_USERS, Type.QR_CODE_SWITCH -> updateUser() } diff --git a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt index 2dc8603827..6bfee40ec4 100644 --- a/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt +++ b/libs/login-api-2/src/main/java/com/instructure/loginapi/login/viewmodel/LoginViewModel.kt @@ -24,7 +24,9 @@ import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.canvasapi2.managers.UserManager import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,14 +41,17 @@ class LoginViewModel @Inject constructor( private val featureFlagProvider: FeatureFlagProvider, private val userManager: UserManager, private val oauthManager: OAuthManager, - private val apiPrefs: ApiPrefs) : ViewModel() { + private val apiPrefs: ApiPrefs, + private val networkStateProvider: NetworkStateProvider) : ViewModel() { private val loginResultAction = MutableLiveData>() fun checkLogin(checkToken: Boolean, checkElementary: Boolean): LiveData> { viewModelScope.launch { try { - if (checkToken) { + val offlineEnabled = featureFlagProvider.offlineEnabled() + val offlineLogin = offlineEnabled && !networkStateProvider.isOnline() + if (checkToken && !offlineLogin) { val selfResult = userManager.getSelfAsync(true).await() if (selfResult.isSuccess) { val canvasForElementary = checkCanvasElementary(checkElementary) @@ -56,7 +61,7 @@ class LoginViewModel @Inject constructor( } } else { val canvasForElementary = checkCanvasElementary(checkElementary) - checkTermsAcceptance(canvasForElementary) + checkTermsAcceptance(canvasForElementary, offlineLogin) } } catch (e: Exception) { loginResultAction.value = Event(LoginResultAction.TokenNotValid) @@ -65,12 +70,13 @@ class LoginViewModel @Inject constructor( return loginResultAction } - private suspend fun checkTermsAcceptance(canvasForElementary: Boolean) { + private suspend fun checkTermsAcceptance(canvasForElementary: Boolean, offlineLogin: Boolean = false) { val authenticatedSession = oauthManager.getAuthenticatedSessionAsync("${apiPrefs.fullDomain}/users/self").await() val requiresTermsAcceptance = authenticatedSession.dataOrNull?.requiresTermsAcceptance ?: false if (requiresTermsAcceptance) { loginResultAction.value = Event(LoginResultAction.ShouldAcceptPolicy(canvasForElementary)) } else { + apiPrefs.checkTokenAfterOfflineLogin = offlineLogin loginResultAction.value = Event(LoginResultAction.Login(canvasForElementary)) } } diff --git a/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt b/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt index fede6cf70e..7d8548de08 100644 --- a/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt +++ b/libs/login-api-2/src/test/java/com/instructure/loginapi/login/viewmodel/LoginViewModelTest.kt @@ -26,7 +26,9 @@ import com.instructure.canvasapi2.models.AuthenticatedSession import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -53,6 +55,7 @@ class LoginViewModelTest { private val userManager: UserManager = mockk(relaxed = true) private val oauthManager: OAuthManager = mockk(relaxed = true) private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val networkStateProvider: NetworkStateProvider = mockk(relaxed = true) private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) @@ -183,8 +186,77 @@ class LoginViewModelTest { assertEquals(LoginResultAction.ShouldAcceptPolicy(false), loginStatus.value!!.getContentIfNotHandled()!!) } + @Test + fun `Set offline login if the user logs in offline and the feature flag is on`() { + // Given + coEvery { featureFlagProvider.getCanvasForElementaryFlag() } returns true + coEvery { featureFlagProvider.offlineEnabled() } returns true + every { networkStateProvider.isOnline() } returns false + every { oauthManager.getAuthenticatedSessionAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(AuthenticatedSession("", requiresTermsAcceptance = false)) + } + every { userManager.getSelfAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(User()) + } + + // When + viewModel = createViewModel() + val loginStatus = viewModel.checkLogin(true, false) + loginStatus.observe(lifecycleOwner, {}) + + // Then + verify { apiPrefs.checkTokenAfterOfflineLogin = true } + assertEquals(LoginResultAction.Login(false), loginStatus.value!!.getContentIfNotHandled()!!) + } + + @Test + fun `Dont Set offline login if the user logs in offline and the feature flag is off`() { + // Given + coEvery { featureFlagProvider.getCanvasForElementaryFlag() } returns true + coEvery { featureFlagProvider.offlineEnabled() } returns false + every { networkStateProvider.isOnline() } returns false + every { oauthManager.getAuthenticatedSessionAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(AuthenticatedSession("", requiresTermsAcceptance = false)) + } + every { userManager.getSelfAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(User()) + } + + // When + viewModel = createViewModel() + val loginStatus = viewModel.checkLogin(true, false) + loginStatus.observe(lifecycleOwner, {}) + + // Then + verify { apiPrefs.checkTokenAfterOfflineLogin = false } + assertEquals(LoginResultAction.Login(false), loginStatus.value!!.getContentIfNotHandled()!!) + } + + @Test + fun `Dont Set offline login if the user logs in online`() { + // Given + coEvery { featureFlagProvider.getCanvasForElementaryFlag() } returns true + coEvery { featureFlagProvider.offlineEnabled() } returns true + every { networkStateProvider.isOnline() } returns true + every { oauthManager.getAuthenticatedSessionAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(AuthenticatedSession("", requiresTermsAcceptance = false)) + } + every { userManager.getSelfAsync(any()) } returns mockk { + coEvery { await() } returns DataResult.Success(User()) + } + + // When + viewModel = createViewModel() + val loginStatus = viewModel.checkLogin(true, false) + loginStatus.observe(lifecycleOwner, {}) + + // Then + verify { apiPrefs.checkTokenAfterOfflineLogin = false } + assertEquals(LoginResultAction.Login(false), loginStatus.value!!.getContentIfNotHandled()!!) + } + private fun createViewModel(): LoginViewModel { - return LoginViewModel(featureFlagProvider, userManager, oauthManager, apiPrefs) + return LoginViewModel(featureFlagProvider, userManager, oauthManager, apiPrefs, networkStateProvider) } } \ No newline at end of file diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt index 7c8a3a3592..4f5a8ad474 100644 --- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt +++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt @@ -34,7 +34,7 @@ enum class Priority { enum class FeatureCategory { ASSIGNMENTS, SUBMISSIONS, LOGIN, COURSE, DASHBOARD, GROUPS, SETTINGS, PAGES, DISCUSSIONS, MODULES, INBOX, GRADES, FILES, EVENTS, PEOPLE, CONFERENCES, COLLABORATIONS, SYLLABUS, TODOS, QUIZZES, NOTIFICATIONS, - ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER + ANNOTATIONS, ANNOUNCEMENTS, COMMENTS, BOOKMARKS, NONE, K5_DASHBOARD, SPEED_GRADER, SYNC_SETTINGS, SYNC_PROGRESS } enum class SecondaryFeatureCategory { diff --git a/libs/pandares/src/main/res/drawable/ic_offline.xml b/libs/pandares/src/main/res/drawable/ic_offline.xml new file mode 100644 index 0000000000..842a1629bd --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_offline.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/libs/pandares/src/main/res/drawable/ic_offline_synced.xml b/libs/pandares/src/main/res/drawable/ic_offline_synced.xml new file mode 100644 index 0000000000..cb59e3ac25 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_offline_synced.xml @@ -0,0 +1,10 @@ + + + diff --git a/libs/pandares/src/main/res/raw/snail.json b/libs/pandares/src/main/res/raw/snail.json new file mode 100644 index 0000000000..fd13b2e38c --- /dev/null +++ b/libs/pandares/src/main/res/raw/snail.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":1600,"w":174,"h":164,"nm":"Comp 1","ddd":0,"assets":[{"id":"79","layers":[{"ddd":0,"ind":196,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10141.250625610352,10086.78239440918],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10158.250625610352,10086.78239440918],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10161.610625610352,10086.78239440918],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10206.250625610352,10086.78239440918],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":198,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":196},{"ddd":0,"ind":197,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":198},{"ddd":0,"refId":"58","w":20000,"h":20000,"ind":199,"ty":0,"nm":"Snail backward","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10500,10500],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10500,10500],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10500,10500],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10500,10500],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[18.7,18.7],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[18.7,18.7],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[18.7,18.7],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":197},{"ddd":0,"ind":200,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":270,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[148,143.99996948242188],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[95.5,143.99996948242188],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[95.5,143.99996948242188],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[95.5,143.99996948242188],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":272,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":270},{"ddd":0,"ind":271,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":272},{"ddd":0,"refId":"74","w":20000,"h":20000,"ind":273,"ty":0,"nm":"Snail forward","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":271},{"ddd":0,"ind":274,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[52.48905897140503,86.66706871986389],"ix":2},"a":{"a":0,"k":[7.92244234812501,7.53936637892857],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":275,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[9.922442436218262,9.539366245269775],"ix":2},"a":{"a":0,"k":[7.92244234812501,7.53936637892857],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":274},{"ddd":0,"ind":276,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":275,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[2.022484696250025,2.106151760590947],[6.797284696250024,6.318651760590953],[7.353584696250024,6.422951760590948],[7.34318469625002,6.443751760590956],[11.84488469625002,6.814951760590958],[7.353584696250024,6.814951760590958],[5.059084696250025,9.25865176059095],[3.850384696250025,10.24465176059095],[1.450084696250023,10.83005176059095],[0.6656846962500254,9.926151760590955],[0.1490846962500214,6.591351760590953],[0.6656846962500254,0.3720517605909492],[2.022484696250025,2.106151760590947],[2.022484696250025,2.106151760590947],[2.022484696250025,2.106151760590947]],"i":[[0,0],[-2.723700000000001,-0.6936000000000035],[-0.190800000000003,-0.03179999999998984],[0.003500000000002501,-0.007000000000005002],[-1.8265999999999991,0],[1.497099999999996,0],[0.8183000000000007,-0.7129000000000048],[0.4382000000000019,-0.3286999999999978],[0.4772999999999996,0.440400000000011],[0.17419999999999902,0.4838000000000022],[0.06159999999999854,1.306799999999996],[-0.5559000000000012,1.111699999999999],[-0.5590000000000046,-1.597799999999992],[0,0],[0,0]],"o":[[0.8560999999999979,2.103700000000003],[0.1800999999999959,0.03770000000000095],[-0.003500000000002501,0.006900000000001683],[1.202100000000002,0.2466999999999899],[-1.497099999999996,0],[-0.6719000000000008,0.8636999999999944],[-0.367600000000003,0.3285999999999945],[-1.215000000000003,0.9111999999999938],[-0.3393999999999977,-0.131299999999996],[-0.2912000000000035,-0.8087000000000018],[-0.3525999999999954,-2.540599999999998],[0.4951000000000008,-0.9902999999999906],[0,0],[0,0],[0.8560999999999979,2.103700000000003]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[5.922442436218262,5.539366245269775],"ix":2},"a":{"a":0,"k":[5.92244234812501,5.53936637892857],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":1,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":277,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[93.50008344650269,27.3563220500946],"ix":2},"a":{"a":0,"k":[14.06308661302094,14.56872884541866],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":278,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[18.06308650970459,18.56872844696045],"ix":2},"a":{"a":0,"k":[14.06308661302094,14.56872884541866],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":277},{"ddd":0,"ind":279,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":278,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[14.311000000000007,19.93475769083733],[11.6206,21.13745769083733],[0,7.678357690837334],[12.3546,0.5072576908373332],[20.119,8.058157690837334],[14.311000000000007,19.93475769083733],[14.311000000000007,19.93475769083733],[14.311000000000007,19.93475769083733]],"i":[[0,0],[0.8648000000000025,-0.2959999999999994],[4.674500000000009,3.7941],[-4.492500000000007,1.6854],[-0.05499999999999261,-1.7453],[5.745999999999995,-3.205199999999998],[0,0],[0,0]],"o":[[-0.9232000000000085,0.514800000000001],[-3.036899999999989,-5.063699999999997],[2.962900000000005,-2.177100000000003],[7.039999999999992,-2.6411],[0.05500000000000682,1.7456999999999994],[0,0],[0,0],[-0.9232000000000085,0.514800000000001]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[10.06308650970459,10.56872844696045],"ix":2},"a":{"a":0,"k":[10.06308661302094,10.56872884541866],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":280,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[20.45449817180634,16.85542488098145],"ix":2},"a":{"a":0,"k":[15.7460026997761,16.85542549479237],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":281,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[19.74600315093994,20.85542488098145],"ix":2},"a":{"a":0,"k":[15.7460026997761,16.85542549479237],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":280},{"ddd":0,"ind":282,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":281,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[7.144905399552202,25.71085098958475],[3.119445399552202,22.30525098958475],[1.240635399552202,7.11849098958475],[12.5848053995522,1.782330989584751],[23.49200539955221,14.41805098958475],[7.144905399552202,25.71085098958475],[7.144905399552202,25.71085098958475],[7.144905399552202,25.71085098958475]],"i":[[0,0],[1.32056,1.475200000000001],[-0.7184100000000004,1.88936],[-6.708799999999998,-5.544840000000001],[-2.408900000000003,-3.4929999999999986],[4.652200000000001,-4.668599999999998],[0,0],[0,0]],"o":[[-1.3229000000000006,-0.8279999999999994],[-5.07973,-5.6737],[0.7180099999999996,-1.88978],[4.332700000000004,3.580669999999999],[-6.133300000000006,2.7515],[0,0],[0,0],[-1.3229000000000006,-0.8279999999999994]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[11.74600315093994,12.85542583465576],"ix":2},"a":{"a":0,"k":[11.7460026997761,12.85542549479237],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":288,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[15],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[15],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[90.75,67.08326721191406],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[85.75,71.33326721191406],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[90.75,67.08],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":290,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":288},{"ddd":0,"ind":289,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":290},{"ddd":0,"refId":"76","w":20000,"h":20000,"ind":291,"ty":0,"nm":"Right eyeball","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10003.44808774583,10005.33761505138],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10003.44808774583,10005.33761505138],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":289},{"ddd":0,"ind":292,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[85.26025438308716,56.61367201805115],"ix":2},"a":{"a":0,"k":[17.7009514007857,27.84314025359105],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":293,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[21.70095062255859,31.8431396484375],"ix":2},"a":{"a":0,"k":[17.7009514007857,27.84314025359105],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":292},{"ddd":0,"ind":294,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":293,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[2.872296785532583,39.50411939933943],[10.87569678553258,47.56031939933943],[11.02749678553258,47.58821939933944],[18.17889678553259,45.87141939933943],[27.11669678553258,30.54201939933943],[11.86869678553258,0.1044193993394273],[7.043196785532587,1.222419399339426],[0.03249678553258661,24.32991939933943],[2.872296785532583,39.50411939933943],[2.872296785532583,39.50411939933943],[2.872296785532583,39.50411939933943]],"i":[[0,0],[-2.984000000000009,-0.5820000000000078],[-0.048499999999989996,-0.008100000000013097],[-2.065400000000011,1.432000000000002],[-1.259100000000004,7.520900000000008],[8.505099999999999,1.4240999999999993],[1.380599999999987,-1.051399999999997],[0.5305999999999926,-11.4314],[-2.089600000000004,-4.6751],[0,0],[0,0]],"o":[[2.122100000000003,4.747399999999999],[0.05310000000000059,0.01059999999999661],[2.1511,0.3600999999999885],[5.040599999999998,-3.493200000000002],[1.984899999999996,-11.8563],[-1.841700000000003,-0.3082999999999991],[-4.115600000000001,3.1342],[-0.2284000000000077,4.910199999999996],[0,0],[0,0],[2.122100000000003,4.747399999999999]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[13.70095157623291,23.8431396484375],"ix":2},"a":{"a":0,"k":[13.7009514007857,23.84314025359105],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":300,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-20],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[40.67365264892578,63.14909362792969],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[33.41904067993164,68.90872955322266],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[40.67,63.15],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":302,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":300},{"ddd":0,"ind":301,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":302},{"ddd":0,"refId":"78","w":20000,"h":20000,"ind":303,"ty":0,"nm":"Left eyeball","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10004.711248764796,10006.159567720319],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10004.711248764796,10006.159567720319],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[10004.711248764796,10006.159567720319],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":301},{"ddd":0,"ind":304,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[34.77264261245728,53.1414201259613],"ix":2},"a":{"a":0,"k":[23.0392186031972,29.04208285957054],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":305,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[27.03921890258789,33.04208374023438],"ix":2},"a":{"a":0,"k":[23.0392186031972,29.04208285957054],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":304},{"ddd":0,"ind":306,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":305,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[13.16617672712502,49.89251399195656],[13.16627672712502,49.89251399195656],[37.57837672712502,30.07251399195655],[25.51777672712502,0.1265139919565534],[10.37177672712502,6.582313991956553],[0.4508767271250225,23.85681399195655],[13.16617672712502,49.89251399195656],[13.16617672712502,49.89251399195656],[13.16617672712502,49.89251399195656]],"i":[[0,0],[-0.00003333333333443989,0],[-2.026599999999995,12.10380000000001],[8.646100000000004,1.447500000000002],[5.152000000000001,-4.789100000000001],[0.9969999999999999,-5.9544],[-9.0766,-1.5193000000000012],[0,0],[0,0]],"o":[[0.00003333333333443989,0],[9.934799999999997,1.663499999999999],[2.2657999999999987,-13.532],[-4.303599999999996,-0.7209000000000003],[-5.213100000000001,4.845900000000004],[-2.1560000000000006,12.8779],[0,0],[0,0],[0.00003333333333443989,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[19.03921890258789,25.04208374023438],"ix":2},"a":{"a":0,"k":[19.0392186031972,25.04208285957054],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":307,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[62.3321008682251,78.77500224113464],"ix":2},"a":{"a":0,"k":[7.650207792657444,3.928793806390381],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":308,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[7.650207996368408,3.928793907165527],"ix":2},"a":{"a":0,"k":[7.650207792657444,3.928793806390381],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":307},{"ddd":0,"ind":309,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":308,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[0.0257077926574496,2.652443887411636],[7.02850779265745,7.643243887411629],[15.27470779265744,5.20534388741163],[8.271907792657451,0.2141438874116375],[0.0257077926574496,2.652443887411636],[0.0257077926574496,2.652443887411636],[0.0257077926574496,2.652443887411636]],"i":[[0,0],[-4.2109999999999985,-0.7051000000000016],[-0.3438000000000017,2.051500000000004],[4.2109999999999985,0.7051000000000016],[0.3432999999999993,-2.051500000000004],[0,0],[0,0]],"o":[[-0.3432999999999993,2.051000000000002],[4.2105999999999995,0.7051000000000016],[0.3432999999999993,-2.0518],[-4.2105999999999995,-0.7047000000000025],[0,0],[0,0],[-0.3432999999999993,2.051000000000002]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[7.650207996368408,3.928793907165527],"ix":2},"a":{"a":0,"k":[7.650207792657444,3.928793806390381],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":310,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[53.26395416259766,56.64922213554382],"ix":2},"a":{"a":0,"k":[53.26395486377385,46.77863544238294],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":311,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[57.26395416259766,50.77863693237305],"ix":2},"a":{"a":0,"k":[53.26395486377385,46.77863544238294],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":310},{"ddd":0,"ind":312,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":311,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[97.93050044771248,57.66636552661369],[43.42530044771248,84.3150655266137],[0.05282044771248373,41.75566552661368],[57.43440044771248,0.641765526613689],[97.93050044771248,57.66636552661369],[97.93050044771248,57.66636552661369],[97.93050044771248,57.66636552661369]],"i":[[0,0],[28.180699999999995,4.718299999999999],[-1.1052082,22.0177],[-28.180699999999995,-4.71795],[3.993500000000012,-23.8507],[0,0],[0,0]],"o":[[-3.796399999999991,22.6781],[-28.1803,-4.717600000000004],[1.19369,-23.773499999999995],[28.1803,4.718],[0,0],[0,0],[-3.796399999999991,22.6781]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[49.26395416259766,42.77863693237305],"ix":2},"a":{"a":0,"k":[49.26395486377385,42.77863544238294],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":313,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[29.17777252197266,102.7233998775482],"ix":2},"a":{"a":0,"k":[20.14257451979861,21.6309295864456],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":314,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[24.14257431030273,25.63092994689941],"ix":2},"a":{"a":0,"k":[20.14257451979861,21.6309295864456],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":313},{"ddd":0,"ind":315,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":314,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[1.079602129419571,4.676180231603425],[18.08040212941957,35.24648023160343],[31.33800212941957,19.64948023160342],[16.19950212941957,9.942880231603425],[9.95160212941957,2.419280231603423],[1.079602129419571,4.676180231603425],[1.079602129419571,4.676180231603425],[1.079602129419571,4.676180231603425]],"i":[[0,0],[-9.7713,-0.4039999999999964],[3.586899999999996,8.422000000000011],[1.7802000000000007,0.8807999999999936],[2.3962,3.1674000000000007],[1.7525,-5.779499999999999],[0,0],[0,0]],"o":[[-4.768280000000001,15.729299999999995],[9.770600000000002,0.4029999999999916],[-3.587799999999994,-8.4225],[-1.779800000000002,-0.8811000000000035],[-2.678799999999999,-3.540899999999993],[0,0],[0,0],[-4.768280000000001,15.729299999999995]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[16.14257431030273,17.63092994689941],"ix":2},"a":{"a":0,"k":[16.14257451979861,17.6309295864456],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":316,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[30.47693872451782,116.4248349666595],"ix":2},"a":{"a":0,"k":[18.63586795707902,22.36448490363062],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":317,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[22.63586807250977,26.3644847869873],"ix":2},"a":{"a":0,"k":[18.63586795707902,22.36448490363062],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":316},{"ddd":0,"ind":318,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":317,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[6.550629568961263,0],[0.4996295689612626,21.018599999999992],[0.4996295689612626,31.8466],[28.84352956896127,33.75759999999998],[28.84352956896127,18.682599999999994],[6.550629568961263,0],[6.550629568961263,0],[6.550629568961263,0]],"i":[[0,0],[-0.9553999999999991,-5.731999999999999],[-0.3632000000000009,-4.540000000000006],[-6.687900000000006,5.733000000000004],[-1.910800000000002,6.051000000000002],[7.9617,18.4716],[0,0],[0,0]],"o":[[-7.3249,5.732599999999991],[0.5530000000000008,3.317999999999998],[1.4422,4.006999999999991],[1.592399999999998,-6.687999999999988],[-11.1464,-7.643000000000001],[0,0],[0,0],[-7.3249,5.732599999999991]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[14.63586807250977,18.3644847869873],"ix":2},"a":{"a":0,"k":[14.63586795707902,18.36448490363062],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":319,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[41.61069631576538,93.68492293357849],"ix":2},"a":{"a":0,"k":[26.32141699687852,27.591407550816],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":320,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[30.3214168548584,31.59140777587891],"ix":2},"a":{"a":0,"k":[26.32141699687852,27.591407550816],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":319},{"ddd":0,"ind":321,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":320,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[28.58012116801767,45.80143660078829],[40.48792116801767,6.032236600788295],[36.22342116801767,0.5782366007882871],[1.191521168017669,37.20243660078829],[28.58012116801767,45.80143660078829],[28.58012116801767,45.80143660078829],[28.58012116801767,45.80143660078829]],"i":[[0,0],[6.358800000000002,12.3922],[16.5605,5.095500000000001],[-7.006299999999998,-11.7834],[-8.280299999999997,2.228999999999999],[0,0],[0,0]],"o":[[19.4267,-4.459000000000003],[-3.627600000000001,-6.090900000000005],[-16.5605,-5.095499999999994],[7.006400000000003,11.783],[0,0],[0,0],[19.4267,-4.459000000000003]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[22.3214168548584,23.59140777587891],"ix":2},"a":{"a":0,"k":[22.32141699687852,23.591407550816],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[1,1,1],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":322,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[15.66900491714478,108.3082301616669],"ix":2},"a":{"a":0,"k":[10.52618450478832,10.42617917858943],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":323,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[14.52618408203125,14.42617893218994],"ix":2},"a":{"a":0,"k":[10.52618450478832,10.42617917858943],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":322},{"ddd":0,"ind":324,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":323,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[4.135279575822701,1.662900000000008],[6.526179575822701,0],[8.9170795758227,1.662900000000008],[11.6960795758227,2.480900000000005],[11.8985795758227,5.401899999999998],[12.9864795758227,8.108900000000006],[10.8344795758227,10.0639],[9.413379575822702,12.5939],[6.526179575822701,12.138900000000007],[3.6390095758227,12.5939],[2.217849575822702,10.0639],[0.06589957582270056,8.108900000000006],[1.153779575822701,5.401899999999998],[1.356239575822701,2.480900000000005],[4.135279575822701,1.662900000000008],[4.135279575822701,1.662900000000008],[4.135279575822701,1.662900000000008]],"i":[[0,0],[-1.0963000000000012,0],[-0.3599999999999994,-0.9709000000000003],[-0.6857000000000006,-0.8470000000000084],[0.5383999999999993,-0.8930000000000007],[0.2470999999999997,-1.070000000000007],[1.0244,-0.1350000000000051],[0.9812000000000012,-0.4789999999999992],[0.7491000000000003,0.7219999999999942],[0.989370000000001,0.4819999999999993],[-0.09646000000000043,1.025999999999996],[0.2461100000000007,1.066000000000003],[-0.8685599999999996,0.5679999999999978],[-0.69414,0.8569999999999993],[-0.9769699999999997,-0.3210000000000122],[0,0],[0,0]],"o":[[0.3599999999999994,-0.9709000000000003],[1.0962999999999994,0],[0.9769000000000005,-0.3210000000000122],[0.6942000000000021,0.8569999999999993],[0.8685000000000009,0.5679999999999978],[-0.246100000000002,1.066000000000003],[0.09649999999999892,1.025999999999996],[-0.9893999999999998,0.4819999999999993],[-0.7491000000000003,0.7219999999999942],[-0.9812899999999996,-0.4789999999999992],[-1.0244400000000011,-0.1350000000000051],[-0.2471099999999993,-1.070000000000007],[-0.5384399999999996,-0.8930000000000007],[0.68567,-0.8470000000000084],[0,0],[0,0],[0.3599999999999994,-0.9709000000000003]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[6.526184558868408,6.4261794090271],"ix":2},"a":{"a":0,"k":[6.526184504788324,6.426179178589429],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":325,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[64.0845217704773,105.678733587265],"ix":2},"a":{"a":0,"k":[13.61282206356619,14.23168014639047],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":326,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[17.61282253265381,18.23167991638184],"ix":2},"a":{"a":0,"k":[13.61282206356619,14.23168014639047],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":325},{"ddd":0,"ind":327,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":326,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[18.8754,8.811900292414151],[0,1.101700292414151],[0.2098000000000013,1.06100029241415],[9.0028,20.12890029241414],[18.8754,8.811900292414151],[18.8754,8.811900292414151],[18.8754,8.811900292414151]],"i":[[0,0],[3.943200000000004,-0.8785000000000025],[-0.06810000000000116,0.01569999999999538],[-10.5095,-2.162999999999997],[0.863900000000001,4.045000000000002],[0,0],[0,0]],"o":[[-2.547799999999995,-11.9311],[0.0688999999999993,-0.01530000000001053],[2.6191999999999993,5.103999999999999],[10.828,2.230000000000004],[0,0],[0,0],[-2.547799999999995,-11.9311]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[9.612822532653809,10.23167991638184],"ix":2},"a":{"a":0,"k":[9.612822063566192,10.23168014639047],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":328,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[52.93664598464966,115.6757733821869],"ix":2},"a":{"a":0,"k":[17.98454405246839,17.64158368089306],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":329,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[21.98454475402832,21.6415843963623],"ix":2},"a":{"a":0,"k":[17.98454405246839,17.64158368089306],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":328},{"ddd":0,"ind":330,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":329,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[1.4210854715202e-14,13.86076186291739],[3.821700000000014,22.14076186291739],[27.70710000000001,23.09676186291739],[20.06370000000001,0.8032618629173811],[1.4210854715202e-14,13.86076186291739],[1.4210854715202e-14,13.86076186291739],[1.4210854715202e-14,13.86076186291739]],"i":[[0,0],[0,-4.671000000000006],[-1.273899999999998,5.731999999999999],[5.414100000000005,4.140500000000003],[9.7665,-11.465],[0,0],[0,0]],"o":[[2.547799999999995,0.8490000000000038],[0,6.688000000000002],[1.273800000000008,-5.733000000000004],[-3.609299999999998,-2.759999999999991],[0,0],[0,0],[2.547799999999995,0.8490000000000038]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[13.984543800354,13.64158344268799],"ix":2},"a":{"a":0,"k":[13.98454405246839,13.64158368089306],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"st","c":{"a":0,"k":[0.08235294117647059,0.14901960784313725,0.2],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":2,"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":0,"k":[0.17647058823529413,0.23137254901960785,0.27058823529411763],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0},{"ddd":0,"ind":331,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[44.42040681838989,130.8979508876801],"ix":2},"a":{"a":0,"k":[36.499995,10],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":200},{"ddd":0,"ind":332,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[36.49999618530273,10],"ix":2},"a":{"a":0,"k":[36.499995,10],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":331},{"ddd":0,"ind":333,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":332,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":0,"k":{"c":false,"v":[[36.49999,20],[72.99999,10],[36.49999,0],[0,10],[36.49999,20],[36.49999,20],[36.49999,20]],"i":[[0,0],[0,5.522999999999996],[20.1584,0],[0,-5.522999999999996],[-20.1584,0],[0,0],[0,0]],"o":[[20.1584,0],[0,-5.522999999999996],[-20.1584,0],[0,5.522999999999996],[0,0],[0,0],[20.1584,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":0,"k":[36.49999618530273,10],"ix":2},"a":{"a":0,"k":[36.499995,10],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":5,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"81","layers":[{"ddd":0,"refId":"80","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"79","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"80","layers":[{"ddd":0,"ind":334,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10090.000000476837,10084.89795088768],"ix":2},"a":{"a":0,"k":[87,82],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":335,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[87,82],"ix":2},"a":{"a":0,"k":[87,82],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":334},{"ddd":0,"ind":336,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[87,82],"ix":2},"a":{"a":0,"k":[87,82],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":335},{"ddd":0,"ind":337,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":336,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":0,"k":[174,164],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":2}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[87,82],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"fl","c":{"a":0,"k":[0,0,0],"ix":2},"o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"58","layers":[{"ddd":0,"ind":32,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10311.955657958984,10731.192993164062],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10311.955657958984,10731.192993164062],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10311.955657958984,10731.192993164062],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":34,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":32},{"ddd":0,"ind":33,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":34},{"ddd":0,"refId":"10","w":20000,"h":20000,"ind":35,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":33},{"ddd":0,"ind":67,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10251.357604980469,10698.538940429688],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10251.357604980469,10698.538940429688],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10251.357604980469,10698.538940429688],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":69,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":67},{"ddd":0,"ind":68,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":69},{"ddd":0,"refId":"20","w":20000,"h":20000,"ind":70,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":68},{"ddd":0,"ind":97,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10230.458801269531,10719.437683105469],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10230.458801269531,10719.437683105469],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10230.458801269531,10719.437683105469],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":99,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":97},{"ddd":0,"ind":98,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":99},{"ddd":0,"refId":"29","w":20000,"h":20000,"ind":100,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":98},{"ddd":0,"ind":127,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10312.32553100586,10775.20361328125],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10312.32553100586,10775.20361328125],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10312.32553100586,10775.20361328125],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":129,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":127},{"ddd":0,"ind":128,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":129},{"ddd":0,"refId":"38","w":20000,"h":20000,"ind":130,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":128},{"ddd":0,"ind":162,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10286.466552734375,10734.975646972656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10286.466552734375,10734.975646972656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10286.466552734375,10734.975646972656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":164,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":162},{"ddd":0,"ind":163,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":164},{"ddd":0,"refId":"48","w":20000,"h":20000,"ind":165,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":163},{"ddd":0,"ind":192,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[180],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10312.328796386719,10870.953674316406],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10312.328796386719,10870.953674316406],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10312.328796386719,10870.953674316406],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":194,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":192},{"ddd":0,"ind":193,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":194},{"ddd":0,"refId":"57","w":20000,"h":20000,"ind":195,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[522.4699974060059,-522.4699974060059],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":193}]},{"id":"8","layers":[{"ddd":0,"ind":15,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":17,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":15},{"ddd":0,"ind":16,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":17},{"ddd":0,"refId":"4","w":20000,"h":20000,"ind":18,"ty":0,"nm":"SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":16}]},{"id":"10","layers":[{"ddd":0,"refId":"9","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"8","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"9","layers":[{"ddd":0,"ind":28,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[9964.008567810059,9955.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9964.008567810059,9955.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9964.008567810059,9955.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":30,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":28},{"ddd":0,"ind":29,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":30},{"ddd":0,"refId":"7","w":20000,"h":20000,"ind":31,"ty":0,"nm":"SvgClip","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":29}]},{"id":"4","layers":[{"ddd":0,"ind":11,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10005.714285850525,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285850525,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285850525,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":13,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":11},{"ddd":0,"ind":12,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":13},{"ddd":0,"refId":"3","w":20000,"h":20000,"ind":14,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":12}]},{"id":"3","layers":[{"ddd":0,"ind":2,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":4,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":2},{"ddd":0,"ind":3,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":4},{"ddd":0,"refId":"1","w":20000,"h":20000,"ind":5,"ty":0,"nm":"Path's solid stroke","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":3},{"ddd":0,"ind":7,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":9,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":7},{"ddd":0,"ind":8,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":9},{"ddd":0,"refId":"2","w":20000,"h":20000,"ind":10,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":8}]},{"id":"1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[3.4285714285714306,11.5],[1.9285714285714306,0]],"i":[[0,0],[-2.999999999999986,3.6670000000000016]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[3.4285714285714306,11.5],[1.9285714285714306,0]],"i":[[0,0],[-2.999999999999986,3.6670000000000016]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[3.4285714285714306,11.5],[1.9285714285714306,0]],"i":[[0,0],[-2.999999999999986,3.6670000000000016]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[1.7142857313156128,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.7142857313156128,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.7142857313156128,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[1.7142857142857153,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.7142857142857153,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.7142857142857153,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"2","layers":[{"ddd":0,"ind":6,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[3.4285714285714306,11.5],[1.9285714285714306,0]],"i":[[0,0],[-2.999999999999986,3.6670000000000016]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[3.4285714285714306,11.5],[1.9285714285714306,0]],"i":[[0,0],[-2.999999999999986,3.6670000000000016]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[3.4285714285714306,11.5],[1.9285714285714306,0]],"i":[[0,0],[-2.999999999999986,3.6670000000000016]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[1.7142857313156128,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.7142857313156128,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.7142857313156128,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[1.7142857142857153,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.7142857142857153,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.7142857142857153,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"7","layers":[{"ddd":0,"ind":24,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":26,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":24},{"ddd":0,"ind":25,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":26},{"ddd":0,"refId":"6","w":20000,"h":20000,"ind":27,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":25}]},{"id":"6","layers":[{"ddd":0,"ind":20,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":22,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":20},{"ddd":0,"ind":21,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":22},{"ddd":0,"refId":"5","w":20000,"h":20000,"ind":23,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":21}]},{"id":"5","layers":[{"ddd":0,"ind":19,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":1,"k":[{"t":800,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"18","layers":[{"ddd":0,"ind":50,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":52,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":50},{"ddd":0,"ind":51,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":52},{"ddd":0,"refId":"14","w":20000,"h":20000,"ind":53,"ty":0,"nm":"SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":51}]},{"id":"20","layers":[{"ddd":0,"refId":"19","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"18","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"19","layers":[{"ddd":0,"ind":63,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[9952.410202026367,9962.000045776367],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9952.410202026367,9962.000045776367],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9952.410202026367,9962.000045776367],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":65,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":63},{"ddd":0,"ind":64,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":65},{"ddd":0,"refId":"17","w":20000,"h":20000,"ind":66,"ty":0,"nm":"SvgClip","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":64}]},{"id":"14","layers":[{"ddd":0,"ind":46,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10018.140405654907,10021.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405654907,10021.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405654907,10021.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":48,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":46},{"ddd":0,"ind":47,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":48},{"ddd":0,"refId":"13","w":20000,"h":20000,"ind":49,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":47}]},{"id":"13","layers":[{"ddd":0,"ind":37,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":39,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":37},{"ddd":0,"ind":38,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":39},{"ddd":0,"refId":"11","w":20000,"h":20000,"ind":40,"ty":0,"nm":"Path's solid stroke","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":38},{"ddd":0,"ind":42,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":44,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":42},{"ddd":0,"ind":43,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":44},{"ddd":0,"refId":"12","w":20000,"h":20000,"ind":45,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":43}]},{"id":"11","layers":[{"ddd":0,"ind":36,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623],[14.469206872896706,8.448044903136633],[9.763206872896717,24.246044903136635],[11.556206872896695,15.059044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.0089999999999861],[-8.739000000000004,-2.1280000000000143],[4.3700000000000045,10.420000000000016]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623],[14.469206872896706,8.448044903136633],[9.763206872896717,24.246044903136635],[11.556206872896695,15.059044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.0089999999999861],[-8.739000000000004,-2.1280000000000143],[4.3700000000000045,10.420000000000016]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623],[14.469206872896706,8.448044903136633],[9.763206872896717,24.246044903136635],[11.556206872896695,15.059044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.0089999999999861],[-8.739000000000004,-2.1280000000000143],[4.3700000000000045,10.420000000000016]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"12","layers":[{"ddd":0,"ind":41,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623],[14.469206872896706,8.448044903136633],[9.763206872896717,24.246044903136635],[11.556206872896695,15.059044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.0089999999999861],[-8.739000000000004,-2.1280000000000143],[4.3700000000000045,10.420000000000016]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623],[14.469206872896706,8.448044903136633],[9.763206872896717,24.246044903136635],[11.556206872896695,15.059044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.0089999999999861],[-8.739000000000004,-2.1280000000000143],[4.3700000000000045,10.420000000000016]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623],[14.469206872896706,8.448044903136633],[9.763206872896717,24.246044903136635],[11.556206872896695,15.059044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.0089999999999861],[-8.739000000000004,-2.1280000000000143],[4.3700000000000045,10.420000000000016]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"17","layers":[{"ddd":0,"ind":59,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":61,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":59},{"ddd":0,"ind":60,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":61},{"ddd":0,"refId":"16","w":20000,"h":20000,"ind":62,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":60}]},{"id":"16","layers":[{"ddd":0,"ind":55,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":57,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":55},{"ddd":0,"ind":56,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":57},{"ddd":0,"refId":"15","w":20000,"h":20000,"ind":58,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":56}]},{"id":"15","layers":[{"ddd":0,"ind":54,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":1,"k":[{"t":800,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"27","layers":[{"ddd":0,"ind":80,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":82,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":80},{"ddd":0,"ind":81,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":82},{"ddd":0,"refId":"23","w":20000,"h":20000,"ind":83,"ty":0,"nm":"SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":81}]},{"id":"29","layers":[{"ddd":0,"refId":"28","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"27","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"28","layers":[{"ddd":0,"ind":93,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[9948.410202026367,9958.000045776367],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9948.410202026367,9958.000045776367],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9948.410202026367,9958.000045776367],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":95,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":93},{"ddd":0,"ind":94,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":95},{"ddd":0,"refId":"26","w":20000,"h":20000,"ind":96,"ty":0,"nm":"SvgClip","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":94}]},{"id":"23","layers":[{"ddd":0,"ind":76,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":78,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":76},{"ddd":0,"ind":77,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":78},{"ddd":0,"refId":"22","w":20000,"h":20000,"ind":79,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":77}]},{"id":"22","layers":[{"ddd":0,"ind":72,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":74,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":72},{"ddd":0,"ind":73,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":74},{"ddd":0,"refId":"21","w":20000,"h":20000,"ind":75,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":73}]},{"id":"21","layers":[{"ddd":0,"ind":71,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[9.804206872896714,31.500044903136626],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.124206872896707,30.297044903136623]],"i":[[0,0],[-5.713999999999999,11.540999999999997],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.1920000000000073,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.255000000000024],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.14040470123291,17.950763702392578],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.140405170652102,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"26","layers":[{"ddd":0,"ind":89,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":91,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":89},{"ddd":0,"ind":90,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":91},{"ddd":0,"refId":"25","w":20000,"h":20000,"ind":92,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":90}]},{"id":"25","layers":[{"ddd":0,"ind":85,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":87,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":85},{"ddd":0,"ind":86,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":87},{"ddd":0,"refId":"24","w":20000,"h":20000,"ind":88,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":86}]},{"id":"24","layers":[{"ddd":0,"ind":84,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":1,"k":[{"t":800,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"36","layers":[{"ddd":0,"ind":110,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":112,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":110},{"ddd":0,"ind":111,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":112},{"ddd":0,"refId":"32","w":20000,"h":20000,"ind":113,"ty":0,"nm":"SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":111}]},{"id":"38","layers":[{"ddd":0,"refId":"37","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"36","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"37","layers":[{"ddd":0,"ind":123,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[9964.079360961914,9947.326522827148],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9964.079360961914,9947.326522827148],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9964.079360961914,9947.326522827148],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":125,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":123},{"ddd":0,"ind":124,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":125},{"ddd":0,"refId":"35","w":20000,"h":20000,"ind":126,"ty":0,"nm":"SvgClip","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":124}]},{"id":"32","layers":[{"ddd":0,"ind":106,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10023.570980072021,10014.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10023.570980072021,10014.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10023.570980072021,10014.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":108,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":106},{"ddd":0,"ind":107,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":108},{"ddd":0,"refId":"31","w":20000,"h":20000,"ind":109,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":107}]},{"id":"31","layers":[{"ddd":0,"ind":102,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":104,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":102},{"ddd":0,"ind":103,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":104},{"ddd":0,"refId":"30","w":20000,"h":20000,"ind":105,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":103}]},{"id":"30","layers":[{"ddd":0,"ind":101,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[18.74337709534521,14.615528624695287],[7.875377095345215,4.306528624695289],[0.14437709534522014,5.203528624695309],[28.379377095345234,20.21752862469529],[31.964377095345213,14.166528624695303],[18.74337709534521,14.615528624695287],[18.74337709534521,14.615528624695287],[18.74337709534521,14.615528624695287]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.1370000000000005],[6.275000000000006,-0.22399999999998954],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.23699999999999477,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.478999999999985,-2.688999999999993],[-6.274000000000001,0.22399999999998954],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[18.74337709534521,14.615528624695287],[7.875377095345215,4.306528624695289],[0.14437709534522014,5.203528624695309],[28.379377095345234,20.21752862469529],[31.964377095345213,14.166528624695303],[18.74337709534521,14.615528624695287],[18.74337709534521,14.615528624695287],[18.74337709534521,14.615528624695287]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.1370000000000005],[6.275000000000006,-0.22399999999998954],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.23699999999999477,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.478999999999985,-2.688999999999993],[-6.274000000000001,0.22399999999998954],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[18.74337709534521,14.615528624695287],[7.875377095345215,4.306528624695289],[0.14437709534522014,5.203528624695309],[28.379377095345234,20.21752862469529],[31.964377095345213,14.166528624695303],[18.74337709534521,14.615528624695287],[18.74337709534521,14.615528624695287],[18.74337709534521,14.615528624695287]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.1370000000000005],[6.275000000000006,-0.22399999999998954],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.23699999999999477,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.478999999999985,-2.688999999999993],[-6.274000000000001,0.22399999999998954],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[19.570980072021484,10.375652313232422],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[19.570980072021484,10.375652313232422],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[19.570980072021484,10.375652313232422],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[19.57098005289697,10.375652561501568],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[19.57098005289697,10.375652561501568],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[19.57098005289697,10.375652561501568],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"35","layers":[{"ddd":0,"ind":119,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":121,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":119},{"ddd":0,"ind":120,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":121},{"ddd":0,"refId":"34","w":20000,"h":20000,"ind":122,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":120}]},{"id":"34","layers":[{"ddd":0,"ind":115,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":117,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":115},{"ddd":0,"ind":116,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":117},{"ddd":0,"refId":"33","w":20000,"h":20000,"ind":118,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":116}]},{"id":"33","layers":[{"ddd":0,"ind":114,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":1,"k":[{"t":800,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"46","layers":[{"ddd":0,"ind":145,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":147,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":145},{"ddd":0,"ind":146,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":147},{"ddd":0,"refId":"42","w":20000,"h":20000,"ind":148,"ty":0,"nm":"SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":146}]},{"id":"48","layers":[{"ddd":0,"refId":"47","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"46","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"47","layers":[{"ddd":0,"ind":158,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[9959.130104064941,9955.026000976562],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9959.130104064941,9955.026000976562],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9959.130104064941,9955.026000976562],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":160,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":158},{"ddd":0,"ind":159,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":160},{"ddd":0,"refId":"45","w":20000,"h":20000,"ind":161,"ty":0,"nm":"SvgClip","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":159}]},{"id":"42","layers":[{"ddd":0,"ind":141,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10006.318552970886,10009.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.318552970886,10009.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.318552970886,10009.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":143,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":141},{"ddd":0,"ind":142,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":143},{"ddd":0,"refId":"41","w":20000,"h":20000,"ind":144,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":142}]},{"id":"41","layers":[{"ddd":0,"ind":132,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":134,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":132},{"ddd":0,"ind":133,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":134},{"ddd":0,"refId":"39","w":20000,"h":20000,"ind":135,"ty":0,"nm":"Path's solid stroke","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":133},{"ddd":0,"ind":137,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":139,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":137},{"ddd":0,"ind":138,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":139},{"ddd":0,"refId":"40","w":20000,"h":20000,"ind":140,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":138}]},{"id":"39","layers":[{"ddd":0,"ind":131,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[0.7531055222792133,10.927000000000021],[4.637105522279228,0]],"i":[[0,0],[-4.3370000000000175,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[0.7531055222792133,10.927000000000021],[4.637105522279228,0]],"i":[[0,0],[-4.3370000000000175,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[0.7531055222792133,10.927000000000021],[4.637105522279228,0]],"i":[[0,0],[-4.3370000000000175,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[2.3185527324676514,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.3185527324676514,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.3185527324676514,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[2.318552761139614,5.4635000000000105],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.318552761139614,5.4635000000000105],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.318552761139614,5.4635000000000105],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"40","layers":[{"ddd":0,"ind":136,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[0.7531055222792133,10.927000000000021],[4.637105522279228,0]],"i":[[0,0],[-4.3370000000000175,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[0.7531055222792133,10.927000000000021],[4.637105522279228,0]],"i":[[0,0],[-4.3370000000000175,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[0.7531055222792133,10.927000000000021],[4.637105522279228,0]],"i":[[0,0],[-4.3370000000000175,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[2.3185527324676514,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.3185527324676514,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.3185527324676514,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[2.318552761139614,5.4635000000000105],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.318552761139614,5.4635000000000105],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.318552761139614,5.4635000000000105],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"45","layers":[{"ddd":0,"ind":154,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":156,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":154},{"ddd":0,"ind":155,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":156},{"ddd":0,"refId":"44","w":20000,"h":20000,"ind":157,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":155}]},{"id":"44","layers":[{"ddd":0,"ind":150,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":152,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":150},{"ddd":0,"ind":151,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":152},{"ddd":0,"refId":"43","w":20000,"h":20000,"ind":153,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":151}]},{"id":"43","layers":[{"ddd":0,"ind":149,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":1,"k":[{"t":800,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"55","layers":[{"ddd":0,"ind":175,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":177,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":175},{"ddd":0,"ind":176,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":177},{"ddd":0,"refId":"51","w":20000,"h":20000,"ind":178,"ty":0,"nm":"SVG Group","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":176}]},{"id":"57","layers":[{"ddd":0,"refId":"56","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group - mask content","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1,"td":1},{"ddd":0,"refId":"55","w":20000,"h":20000,"ind":1,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[10000,10000],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":0,"tt":1}]},{"id":"56","layers":[{"ddd":0,"ind":188,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[9964.080001831055,9929],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9964.080001831055,9929],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9964.080001831055,9929],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":190,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":188},{"ddd":0,"ind":189,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":190},{"ddd":0,"refId":"54","w":20000,"h":20000,"ind":191,"ty":0,"nm":"SvgClip","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":189}]},{"id":"51","layers":[{"ddd":0,"ind":171,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":173,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":171},{"ddd":0,"ind":172,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":173},{"ddd":0,"refId":"50","w":20000,"h":20000,"ind":174,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":172}]},{"id":"50","layers":[{"ddd":0,"ind":167,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":169,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":167},{"ddd":0,"ind":168,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":169},{"ddd":0,"refId":"49","w":20000,"h":20000,"ind":170,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":168}]},{"id":"49","layers":[{"ddd":0,"ind":166,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084000000000003,0],[0,-3.038000000000011],[-14.082999999999998,0],[0,0],[0,0]],"o":[[14.084000000000003,0],[0,-3.038000000000011],[-14.082999999999998,0],[0,3.038000000000011],[0,0],[0,0],[14.084000000000003,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084000000000003,0],[0,-3.038000000000011],[-14.082999999999998,0],[0,0],[0,0]],"o":[[14.084000000000003,0],[0,-3.038000000000011],[-14.082999999999998,0],[0,3.038000000000011],[0,0],[0,0],[14.084000000000003,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084000000000003,0],[0,-3.038000000000011],[-14.082999999999998,0],[0,0],[0,0]],"o":[[14.084000000000003,0],[0,-3.038000000000011],[-14.082999999999998,0],[0,3.038000000000011],[0,0],[0,0],[14.084000000000003,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"54","layers":[{"ddd":0,"ind":184,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":186,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":184},{"ddd":0,"ind":185,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":186},{"ddd":0,"refId":"53","w":20000,"h":20000,"ind":187,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":185}]},{"id":"53","layers":[{"ddd":0,"ind":180,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":182,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":180},{"ddd":0,"ind":181,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":182},{"ddd":0,"refId":"52","w":20000,"h":20000,"ind":183,"ty":0,"nm":"Rectangle","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10087,10082],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":181}]},{"id":"52","layers":[{"ddd":0,"ind":179,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"rc","hd":false,"d":1,"s":{"a":1,"k":[{"t":800,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[174,164],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[87,82],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"74","layers":[{"ddd":0,"ind":211,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[9983.285720825195,9994.897979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[9983.285720825195,9994.897979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9983.285720825195,9994.897979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9983.285720825195,9994.897979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":213,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":211},{"ddd":0,"ind":212,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":213},{"ddd":0,"refId":"61","w":20000,"h":20000,"ind":214,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":212},{"ddd":0,"ind":225,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":227,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":225},{"ddd":0,"ind":226,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":227},{"ddd":0,"refId":"64","w":20000,"h":20000,"ind":228,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":226},{"ddd":0,"ind":234,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10007.31021118164,10000.848693847656],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":236,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":234},{"ddd":0,"ind":235,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":236},{"ddd":0,"refId":"66","w":20000,"h":20000,"ind":237,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":235},{"ddd":0,"ind":243,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10001.07160949707,10007.94711303711],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10001.07160949707,10007.94711303711],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10001.07160949707,10007.94711303711],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10001.07160949707,10007.94711303711],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":245,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":243},{"ddd":0,"ind":244,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":245},{"ddd":0,"refId":"68","w":20000,"h":20000,"ind":246,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":244},{"ddd":0,"ind":257,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[9988.768447875977,9995.335479736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[9988.768447875977,9995.335479736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[9988.768447875977,9995.335479736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[9988.768447875977,9995.335479736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":259,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":257},{"ddd":0,"ind":258,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":259},{"ddd":0,"refId":"71","w":20000,"h":20000,"ind":260,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":258},{"ddd":0,"ind":266,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10003,10017.397979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10003,10017.397979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10003,10017.397979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10003,10017.397979736328],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":268,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":266},{"ddd":0,"ind":267,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":268},{"ddd":0,"refId":"73","w":20000,"h":20000,"ind":269,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":267}]},{"id":"61","layers":[{"ddd":0,"ind":202,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":204,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":202},{"ddd":0,"ind":203,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":204},{"ddd":0,"refId":"59","w":20000,"h":20000,"ind":205,"ty":0,"nm":"Path's solid stroke","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":203},{"ddd":0,"ind":207,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10009.714285850525,10013.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":209,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":207},{"ddd":0,"ind":208,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":209},{"ddd":0,"refId":"60","w":20000,"h":20000,"ind":210,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10005.714285714286,10009.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":208}]},{"id":"59","layers":[{"ddd":0,"ind":201,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":0,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":0,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"60","layers":[{"ddd":0,"ind":206,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[3.428571428571431,11.5],[1.928571428571431,0]],"i":[[0,0],[-2.999999999999986,3.667000000000002]],"o":[[-3.999999999999986,-4],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.714285731315613,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1.714285714285715,5.75],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"64","layers":[{"ddd":0,"ind":216,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":218,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":216},{"ddd":0,"ind":217,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":218},{"ddd":0,"refId":"62","w":20000,"h":20000,"ind":219,"ty":0,"nm":"Path's solid stroke","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":217},{"ddd":0,"ind":221,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10022.140405654907,10025.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":223,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":221},{"ddd":0,"ind":222,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":223},{"ddd":0,"refId":"63","w":20000,"h":20000,"ind":224,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10018.140405170652,10021.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":222}]},{"id":"62","layers":[{"ddd":0,"ind":215,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":0,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":0,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"63","layers":[{"ddd":0,"ind":220,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662],[14.46920687289671,8.448044903136633],[9.763206872896717,24.24604490313664],[11.55620687289669,15.05904490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004],[8.067000000000007,1.008999999999986],[-8.739000000000004,-2.128000000000014],[4.370000000000005,10.42000000000002]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[10.420999999999992,-5.602000000000004],[-8.067000000000007,-1.0080000000000098],[0,0],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"66","layers":[{"ddd":0,"ind":230,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140404701233,10017.950763702393],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":232,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":230},{"ddd":0,"ind":231,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":232},{"ddd":0,"refId":"65","w":20000,"h":20000,"ind":233,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10014.140405170652,10017.950764637075],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":231}]},{"id":"65","layers":[{"ddd":0,"ind":229,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[9.804206872896714,31.50004490313663],[26.45820687289671,26.48704490313662],[7.746206872896721,5.759044903136612],[13.12420687289671,30.29704490313662]],"i":[[0,0],[-5.713999999999999,11.541],[15.238,-17.25499999999998],[-10.419999999999987,5.602000000000004]],"o":[[1.192000000000007,6.527999999999992],[5.998999999999995,-12.115999999999985],[-15.238,17.25500000000002],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.14040470123291,17.95076370239258],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[14.1404051706521,17.95076463707443],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1,0.9058823529411765,0.6078431372549019],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"68","layers":[{"ddd":0,"ind":239,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10027.570980072021,10018.375652313232],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":241,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":239},{"ddd":0,"ind":240,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":241},{"ddd":0,"refId":"67","w":20000,"h":20000,"ind":242,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10023.570980052897,10014.3756525615],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":240}]},{"id":"67","layers":[{"ddd":0,"ind":238,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[18.74337709534521,14.61552862469529],[7.875377095345215,4.306528624695289],[0.1443770953452201,5.203528624695309],[28.37937709534523,20.21752862469529],[31.96437709534521,14.1665286246953],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.137],[6.275000000000006,-0.2239999999999895],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.2369999999999948,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.47899999999998,-2.688999999999993],[-6.274000000000001,0.2239999999999895],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[18.74337709534521,14.61552862469529],[7.875377095345215,4.306528624695289],[0.1443770953452201,5.203528624695309],[28.37937709534523,20.21752862469529],[31.96437709534521,14.1665286246953],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.137],[6.275000000000006,-0.2239999999999895],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.2369999999999948,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.47899999999998,-2.688999999999993],[-6.274000000000001,0.2239999999999895],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[18.74337709534521,14.61552862469529],[7.875377095345215,4.306528624695289],[0.1443770953452201,5.203528624695309],[28.37937709534523,20.21752862469529],[31.96437709534521,14.1665286246953],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.137],[6.275000000000006,-0.2239999999999895],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.2369999999999948,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.47899999999998,-2.688999999999993],[-6.274000000000001,0.2239999999999895],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[18.74337709534521,14.61552862469529],[7.875377095345215,4.306528624695289],[0.1443770953452201,5.203528624695309],[28.37937709534523,20.21752862469529],[31.96437709534521,14.1665286246953],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529],[18.74337709534521,14.61552862469529]],"i":[[0,0],[-0.3359999999999843,8.403999999999996],[0.8960000000000008,-6.723000000000013],[-26.555000000000007,3.137],[6.275000000000006,-0.2239999999999895],[0,0],[0,0],[0,0]],"o":[[-7.619,-1.9050000000000011],[0.2369999999999948,-5.936999999999983],[-0.8969999999999914,6.72199999999998],[17.47899999999998,-2.688999999999993],[-6.274000000000001,0.2239999999999895],[0,0],[0,0],[-7.619,-1.9050000000000011]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[19.57098007202148,10.37565231323242],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[19.57098007202148,10.37565231323242],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[19.57098007202148,10.37565231323242],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[19.57098007202148,10.37565231323242],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[19.57098005289697,10.37565256150157],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[19.57098005289697,10.37565256150157],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[19.57098005289697,10.37565256150157],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[19.57098005289697,10.37565256150157],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":0,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":0,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.00392156862745098,0.7254901960784313,0.6392156862745098],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"71","layers":[{"ddd":0,"ind":248,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":250,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":248},{"ddd":0,"ind":249,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":250},{"ddd":0,"refId":"69","w":20000,"h":20000,"ind":251,"ty":0,"nm":"Path's solid stroke","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":249},{"ddd":0,"ind":253,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10010.318552970886,10013.463500022888],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":255,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":253},{"ddd":0,"ind":254,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":255},{"ddd":0,"refId":"70","w":20000,"h":20000,"ind":256,"ty":0,"nm":"Path's solid fill","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10006.31855276114,10009.4635],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":254}]},{"id":"69","layers":[{"ddd":0,"ind":247,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"st","c":{"a":1,"k":[{"t":0,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0.08235294117647059,0.14901960784313725,0.2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"w":{"a":1,"k":[{"t":0,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"hd":false,"lc":2,"lj":2},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"70","layers":[{"ddd":0,"ind":252,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[0.7531055222792133,10.92700000000002],[4.637105522279228,0]],"i":[[0,0],[-4.337000000000018,1.9050000000000011]],"o":[[-1.7479999999999905,-5.379000000000019],[0,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.318552732467651,5.463500022888184],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[2.318552761139614,5.46350000000001],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"73","layers":[{"ddd":0,"ind":262,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":264,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":262},{"ddd":0,"ind":263,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":264},{"ddd":0,"refId":"72","w":20000,"h":20000,"ind":265,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10025.5,10005.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":263}]},{"id":"72","layers":[{"ddd":0,"ind":261,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,0],[0,0]],"o":[[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,3.038000000000011],[0,0],[0,0],[14.084,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,0],[0,0]],"o":[[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,3.038000000000011],[0,0],[0,0],[14.084,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,0],[0,0]],"o":[[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,3.038000000000011],[0,0],[0,0],[14.084,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[25.500000000000014,11],[51.000000000000014,5.5],[25.500000000000014,0],[2.842170943040401e-14,5.5],[25.500000000000014,11],[25.500000000000014,11],[25.500000000000014,11]],"i":[[0,0],[0,3.038000000000011],[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,0],[0,0]],"o":[[14.084,0],[0,-3.038000000000011],[-14.083,0],[0,3.038000000000011],[0,0],[0,0],[14.084,0]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[25.5,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[25.50000000000002,5.5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[5],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"76","layers":[{"ddd":0,"ind":284,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":800,"s":[10003.44808769226,10005.337615013123],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10003.44808769226,10005.337615013123],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":286,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":800,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":284},{"ddd":0,"ind":285,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":286},{"ddd":0,"refId":"75","w":20000,"h":20000,"ind":287,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[10003.44808774583,10005.33761505138],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10003.44808774583,10005.33761505138],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":285}]},{"id":"75","layers":[{"ddd":0,"ind":283,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":800,"s":[{"c":false,"v":[[6.834629005212363,4.92670728301691],[4.093729005212353,10.65910728301692],[0.06162900521235315,5.748507283016913],[2.802129005212365,0.01610728301691466],[6.834629005212363,4.92670728301691],[6.834629005212363,4.92670728301691],[6.834629005212363,4.92670728301691]],"i":[[0,0],[1.870200000000011,-0.2267000000000081],[0.3567000000000036,2.939000000000007],[-1.869800000000012,0.22710000000000008],[-0.3567000000000036,-2.938599999999994],[0,0],[0,0]],"o":[[0.3562999999999903,2.939],[-1.870599999999996,0.227099999999993],[-0.3566000000000003,-2.939],[1.870199999999997,-0.2270000000000039],[0,0],[0,0],[0.3562999999999903,2.939]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[{"c":false,"v":[[6.834629005212363,4.92670728301691],[4.093729005212353,10.65910728301692],[0.06162900521235315,5.748507283016913],[2.802129005212365,0.01610728301691466],[6.834629005212363,4.92670728301691],[6.834629005212363,4.92670728301691],[6.834629005212363,4.92670728301691]],"i":[[0,0],[1.870200000000011,-0.2267000000000081],[0.3567000000000036,2.939000000000007],[-1.869800000000012,0.22710000000000008],[-0.3567000000000036,-2.938599999999994],[0,0],[0,0]],"o":[[0.3562999999999903,2.939],[-1.870599999999996,0.227099999999993],[-0.3566000000000003,-2.939],[1.870199999999997,-0.2270000000000039],[0,0],[0,0],[0.3562999999999903,2.939]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[3.448087692260742,5.337615013122559],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[3.448087692260742,5.337615013122559],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[3.448087745829369,5.337615051379704],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[3.448087745829369,5.337615051379704],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":800,"s":[1,1,1],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[1,1,1],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":800,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":800,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":800,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":800,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]},{"id":"78","layers":[{"ddd":0,"ind":296,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":1,"k":[{"t":0,"s":[10004.711248874664,10006.159567832947],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10004.711248874664,10006.159567832947],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[10004.711248874664,10006.159567832947],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false},{"ddd":0,"ind":298,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":1,"k":[{"t":0,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":296},{"ddd":0,"ind":297,"ty":3,"nm":"","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":298},{"ddd":0,"refId":"77","w":20000,"h":20000,"ind":299,"ty":0,"nm":"Path","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[-45],"i":{"x":[0],"y":[0]},"o":{"x":[0],"y":[0]}}],"ix":2},"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[10004.711248764796,10006.159567720319],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[10004.711248764796,10006.159567720319],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[10004.711248764796,10006.159567720319],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false,"parent":297}]},{"id":"77","layers":[{"ddd":0,"ind":295,"ty":4,"nm":"Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[10000,10000],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"hd":false,"parent":0,"shapes":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","hd":false,"d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"v":[[7.571295972065286,0.2766734983580221],[8.45489597206528,7.979473498358018],[1.85149597206528,12.04247349835802],[0.9674959720652865,4.339673498358017],[7.571295972065286,0.2766734983580221],[7.571295972065286,0.2766734983580221],[7.571295972065286,0.2766734983580221]],"i":[[0,0],[1.579300000000003,-3.2486999999999995],[2.067500000000003,1.0048999999999992],[-1.579400000000007,3.249000000000002],[-2.067500000000003,-1.005000000000003],[0,0],[0,0]],"o":[[2.067499999999995,1.004999999999995],[-1.579299999999996,3.2494000000000014],[-2.067499999999995,-1.005400000000002],[1.579699999999995,-3.2490999999999985],[0,0],[0,0],[2.067499999999995,1.004999999999995]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[{"c":false,"v":[[7.571295972065286,0.2766734983580221],[8.45489597206528,7.979473498358018],[1.85149597206528,12.04247349835802],[0.9674959720652865,4.339673498358017],[7.571295972065286,0.2766734983580221],[7.571295972065286,0.2766734983580221],[7.571295972065286,0.2766734983580221]],"i":[[0,0],[1.579300000000003,-3.2486999999999995],[2.067500000000003,1.0048999999999992],[-1.579400000000007,3.249000000000002],[-2.067500000000003,-1.005000000000003],[0,0],[0,0]],"o":[[2.067499999999995,1.004999999999995],[-1.579299999999996,3.2494000000000014],[-2.067499999999995,-1.005400000000002],[1.579699999999995,-3.2490999999999985],[0,0],[0,0],[2.067499999999995,1.004999999999995]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[{"c":false,"v":[[7.571295972065286,0.2766734983580221],[8.45489597206528,7.979473498358018],[1.85149597206528,12.04247349835802],[0.9674959720652865,4.339673498358017],[7.571295972065286,0.2766734983580221],[7.571295972065286,0.2766734983580221],[7.571295972065286,0.2766734983580221]],"i":[[0,0],[1.579300000000003,-3.2486999999999995],[2.067500000000003,1.0048999999999992],[-1.579400000000007,3.249000000000002],[-2.067500000000003,-1.005000000000003],[0,0],[0,0]],"o":[[2.067499999999995,1.004999999999995],[-1.579299999999996,3.2494000000000014],[-2.067499999999995,-1.005400000000002],[1.579699999999995,-3.2490999999999985],[0,0],[0,0],[2.067499999999995,1.004999999999995]]}],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"m":1,"hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[4.711248874664307,6.159567832946777],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[4.711248874664307,6.159567832946777],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[4.711248874664307,6.159567832946777],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[4.711248764795673,6.159567720317721],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[4.711248764795673,6.159567720317721],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[4.711248764795673,6.159567720317721],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]},{"ty":"fl","c":{"a":1,"k":[{"t":0,"s":[1,1,1],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[1,1,1],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[1,1,1],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","hd":false,"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0,0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100,100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[100],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sk":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"sa":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":801,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1594,"s":[0],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2}}]}],"ip":0,"op":1600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"refId":"81","w":20000,"h":20000,"ind":338,"ty":0,"nm":"Clipped SVG Group","sr":1,"ks":{"o":{"a":0,"k":100,"ix":2},"r":{"a":0,"k":0,"ix":2},"p":{"a":0,"k":[2.249999761581421,-2.8979492187499996],"ix":2},"a":{"a":1,"k":[{"t":816,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"t":1600,"s":[10000,10000],"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}],"ix":2},"s":{"a":0,"k":[100,100],"ix":2}},"ao":0,"ip":0,"op":1600,"st":0,"bm":0,"hd":false}],"markers":[]} \ No newline at end of file diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 01f7f5f4cd..27c926147c 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1261,7 +1261,7 @@ Anonymous discussions are currently not supported on mobile. Open in browser to view discussion. Open in browser - + Switch to list view Switch to grid view @@ -1401,4 +1401,89 @@ Version There was a problem reloading this assignment. Please check your connection and try again. Instructure logo + Preferences + Offline Content + Synchronization + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas Student + Remaining + All Courses + Sync + %d Selected + Select All + Deselect All + An error occurred while loading the content. + No Courses + Your courses will be listed here, and then you can make them available for offline usage. + No Course Content + The course content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off then no synchronization will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronization. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled the content synchronization will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronization + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled the content synchronization will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more files failed to sync. Check your internet connection and retry to submit. + Download starting + Courses cannot be added to favorites offline. + All Courses + Courses + Groups + All Courses + Selecting courses for Dashboard can only be done online. You can navigate to offline course details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d course is syncing. + %d courses are syncing. + + This assignment is no longer available. + You are offline + You currently don\'t have any courses that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d course has been synced. + %d courses have been synced. + + Additional course content diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/7.json b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/7.json index de38825764..dba6faef34 100644 --- a/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/7.json +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.appdatabase.AppDatabase/7.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 7, - "identityHash": "0d3dd1cb3dde3ba7d7a93ccddf7ccbf9", + "identityHash": "fb05592c38126b79cd49f3ffb5768382", "entities": [ { "tableName": "AttachmentEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -79,6 +79,12 @@ "columnName": "submissionId", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -134,32 +140,6 @@ "indices": [], "foreignKeys": [] }, - { - "tableName": "EnvironmentFeatureFlags", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `featureFlags` TEXT NOT NULL, PRIMARY KEY(`userId`))", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "featureFlags", - "columnName": "featureFlags", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userId" - ] - }, - "indices": [], - "foreignKeys": [] - }, { "tableName": "FileUploadInputEntity", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `courseId` INTEGER, `assignmentId` INTEGER, `quizId` INTEGER, `quizQuestionId` INTEGER, `position` INTEGER, `parentFolderId` INTEGER, `action` TEXT NOT NULL, `userId` INTEGER, `attachments` TEXT NOT NULL, `submissionId` INTEGER, `filePaths` TEXT NOT NULL, `attemptId` INTEGER, `notificationId` INTEGER, PRIMARY KEY(`workerId`))", @@ -498,7 +478,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0d3dd1cb3dde3ba7d7a93ccddf7ccbf9')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fb05592c38126b79cd49f3ffb5768382')" ] } } \ No newline at end of file diff --git a/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json new file mode 100644 index 0000000000..7e8f420426 --- /dev/null +++ b/libs/pandautils/schemas/com.instructure.pandautils.room.offline.OfflineDatabase/1.json @@ -0,0 +1,5519 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "f792290082e6cf2f70533d168ec4901b", + "entities": [ + { + "tableName": "AssignmentDueDateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `assignmentOverrideId` INTEGER, `dueAt` TEXT, `title` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `isBase` INTEGER NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBase", + "columnName": "isBase", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `submissionTypesRaw` TEXT NOT NULL, `dueAt` TEXT, `pointsPossible` REAL NOT NULL, `courseId` INTEGER NOT NULL, `isGradeGroupsIndividually` INTEGER NOT NULL, `gradingType` TEXT, `needsGradingCount` INTEGER NOT NULL, `htmlUrl` TEXT, `url` TEXT, `quizId` INTEGER NOT NULL, `isUseRubricForGrading` INTEGER NOT NULL, `rubricSettingsId` INTEGER, `allowedExtensions` TEXT NOT NULL, `submissionId` INTEGER, `assignmentGroupId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `isPeerReviews` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockAt` TEXT, `unlockAt` TEXT, `lockExplanation` TEXT, `discussionTopicHeaderId` INTEGER, `freeFormCriterionComments` INTEGER NOT NULL, `published` INTEGER NOT NULL, `groupCategoryId` INTEGER NOT NULL, `userSubmitted` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `onlyVisibleToOverrides` INTEGER NOT NULL, `anonymousPeerReviews` INTEGER NOT NULL, `moderatedGrading` INTEGER NOT NULL, `anonymousGrading` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `plannerOverrideId` INTEGER, `isStudioEnabled` INTEGER NOT NULL, `inClosedGradingPeriod` INTEGER NOT NULL, `annotatableAttachmentId` INTEGER NOT NULL, `anonymousSubmissions` INTEGER NOT NULL, `omitFromFinalGrade` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentGroupId`) REFERENCES `AssignmentGroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionTypesRaw", + "columnName": "submissionTypesRaw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGradeGroupsIndividually", + "columnName": "isGradeGroupsIndividually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingType", + "columnName": "gradingType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizId", + "columnName": "quizId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUseRubricForGrading", + "columnName": "isUseRubricForGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricSettingsId", + "columnName": "rubricSettingsId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "allowedExtensions", + "columnName": "allowedExtensions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPeerReviews", + "columnName": "isPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSubmitted", + "columnName": "userSubmitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyVisibleToOverrides", + "columnName": "onlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousPeerReviews", + "columnName": "anonymousPeerReviews", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moderatedGrading", + "columnName": "moderatedGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousGrading", + "columnName": "anonymousGrading", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannerOverrideId", + "columnName": "plannerOverrideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isStudioEnabled", + "columnName": "isStudioEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inClosedGradingPeriod", + "columnName": "inClosedGradingPeriod", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "annotatableAttachmentId", + "columnName": "annotatableAttachmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "anonymousSubmissions", + "columnName": "anonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "omitFromFinalGrade", + "columnName": "omitFromFinalGrade", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentGroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentGroupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentGroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `position` INTEGER NOT NULL, `groupWeight` REAL NOT NULL, `rules` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupWeight", + "columnName": "groupWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `title` TEXT, `dueAt` INTEGER, `isAllDay` INTEGER NOT NULL, `allDayDate` TEXT, `unlockAt` INTEGER, `lockAt` INTEGER, `courseSectionId` INTEGER NOT NULL, `groupId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayDate", + "columnName": "allDayDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentRubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `rubricId` TEXT NOT NULL, PRIMARY KEY(`assignmentId`, `rubricId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rubricId", + "columnName": "rubricId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId", + "rubricId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentScoreStatisticsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `mean` REAL NOT NULL, `min` REAL NOT NULL, `max` REAL NOT NULL, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mean", + "columnName": "mean", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "min", + "columnName": "min", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AssignmentSetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `scoringRangeId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `position` INTEGER NOT NULL, `masteryPathId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`masteryPathId`) REFERENCES `MasteryPathEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringRangeId", + "columnName": "scoringRangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "masteryPathId", + "columnName": "masteryPathId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "MasteryPathEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "masteryPathId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `originalName` TEXT, `courseCode` TEXT, `startAt` TEXT, `endAt` TEXT, `syllabusBody` TEXT, `hideFinalGrades` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `license` TEXT NOT NULL, `termId` INTEGER, `needsGradingCount` INTEGER NOT NULL, `isApplyAssignmentGroupWeights` INTEGER NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, `isFavorite` INTEGER NOT NULL, `accessRestrictedByDate` INTEGER NOT NULL, `imageUrl` TEXT, `bannerImageUrl` TEXT, `isWeightedGradingPeriods` INTEGER NOT NULL, `hasGradingPeriods` INTEGER NOT NULL, `homePage` TEXT, `restrictEnrollmentsToCourseDate` INTEGER NOT NULL, `workflowState` TEXT, `homeroomCourse` INTEGER NOT NULL, `courseColor` TEXT, `gradingScheme` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`termId`) REFERENCES `TermEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syllabusBody", + "columnName": "syllabusBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideFinalGrades", + "columnName": "hideFinalGrades", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termId", + "columnName": "termId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isApplyAssignmentGroupWeights", + "columnName": "isApplyAssignmentGroupWeights", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessRestrictedByDate", + "columnName": "accessRestrictedByDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageUrl", + "columnName": "imageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bannerImageUrl", + "columnName": "bannerImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isWeightedGradingPeriods", + "columnName": "isWeightedGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasGradingPeriods", + "columnName": "hasGradingPeriods", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homePage", + "columnName": "homePage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "restrictEnrollmentsToCourseDate", + "columnName": "restrictEnrollmentsToCourseDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeroomCourse", + "columnName": "homeroomCourse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseColor", + "columnName": "courseColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "gradingScheme", + "columnName": "gradingScheme", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "TermEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "termId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFilesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`courseId`, `url`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "CourseGradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `gradingPeriodId` INTEGER NOT NULL, PRIMARY KEY(`courseId`, `gradingPeriodId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`gradingPeriodId`) REFERENCES `GradingPeriodEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId", + "gradingPeriodId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "GradingPeriodEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "gradingPeriodId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseSummary` INTEGER, `restrictQuantitativeData` INTEGER NOT NULL, PRIMARY KEY(`courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseSummary", + "columnName": "courseSummary", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "restrictQuantitativeData", + "columnName": "restrictQuantitativeData", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `courseName` TEXT NOT NULL, `fullContentSync` INTEGER NOT NULL, `tabs` TEXT NOT NULL, `fullFileSync` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullContentSync", + "columnName": "fullContentSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullFileSync", + "columnName": "fullFileSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DashboardCardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isK5Subject` INTEGER NOT NULL, `shortName` TEXT, `originalName` TEXT, `courseCode` TEXT, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isK5Subject", + "columnName": "isK5Subject", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originalName", + "columnName": "originalName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseCode", + "columnName": "courseCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionEntryAttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionEntryId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionEntryId`, `remoteFileId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionEntryId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `updatedAt` TEXT, `createdAt` TEXT, `authorId` INTEGER, `description` TEXT, `userId` INTEGER NOT NULL, `parentId` INTEGER NOT NULL, `message` TEXT, `deleted` INTEGER NOT NULL, `totalChildren` INTEGER NOT NULL, `unreadChildren` INTEGER NOT NULL, `ratingCount` INTEGER NOT NULL, `ratingSum` INTEGER NOT NULL, `editorId` INTEGER NOT NULL, `_hasRated` INTEGER NOT NULL, `replyIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChildren", + "columnName": "totalChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadChildren", + "columnName": "unreadChildren", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingCount", + "columnName": "ratingCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingSum", + "columnName": "ratingSum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editorId", + "columnName": "editorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_hasRated", + "columnName": "_hasRated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyIds", + "columnName": "replyIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionParticipantEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `pronouns` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DiscussionTopicHeaderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `discussionType` TEXT, `title` TEXT, `message` TEXT, `htmlUrl` TEXT, `postedDate` INTEGER, `delayedPostDate` INTEGER, `lastReplyDate` INTEGER, `requireInitialPost` INTEGER NOT NULL, `discussionSubentryCount` INTEGER NOT NULL, `readState` TEXT, `unreadCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `assignmentId` INTEGER, `locked` INTEGER NOT NULL, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `pinned` INTEGER NOT NULL, `authorId` INTEGER, `podcastUrl` TEXT, `groupCategoryId` TEXT, `announcement` INTEGER NOT NULL, `permissionId` INTEGER, `published` INTEGER NOT NULL, `allowRating` INTEGER NOT NULL, `onlyGradersCanRate` INTEGER NOT NULL, `sortByRating` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `lockAt` INTEGER, `userCanSeePosts` INTEGER NOT NULL, `specificSections` TEXT, `anonymousState` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`authorId`) REFERENCES `DiscussionParticipantEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`permissionId`) REFERENCES `DiscussionTopicPermissionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionType", + "columnName": "discussionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedDate", + "columnName": "postedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "delayedPostDate", + "columnName": "delayedPostDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastReplyDate", + "columnName": "lastReplyDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "requireInitialPost", + "columnName": "requireInitialPost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionSubentryCount", + "columnName": "discussionSubentryCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readState", + "columnName": "readState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "podcastUrl", + "columnName": "podcastUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "announcement", + "columnName": "announcement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissionId", + "columnName": "permissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowRating", + "columnName": "allowRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlyGradersCanRate", + "columnName": "onlyGradersCanRate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortByRating", + "columnName": "sortByRating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userCanSeePosts", + "columnName": "userCanSeePosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specificSections", + "columnName": "specificSections", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "anonymousState", + "columnName": "anonymousState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionParticipantEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "authorId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "DiscussionTopicPermissionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "permissionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicPermissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `discussionTopicHeaderId` INTEGER NOT NULL, `attach` INTEGER NOT NULL, `update` INTEGER NOT NULL, `delete` INTEGER NOT NULL, `reply` INTEGER NOT NULL, FOREIGN KEY(`discussionTopicHeaderId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionTopicHeaderId", + "columnName": "discussionTopicHeaderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attach", + "columnName": "attach", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delete", + "columnName": "delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicHeaderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicRemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionId` INTEGER NOT NULL, `remoteFileId` INTEGER NOT NULL, PRIMARY KEY(`discussionId`, `remoteFileId`), FOREIGN KEY(`discussionId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`remoteFileId`) REFERENCES `RemoteFileEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionId", + "columnName": "discussionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteFileId", + "columnName": "remoteFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionId", + "remoteFileId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "RemoteFileEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "remoteFileId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DiscussionTopicSectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`discussionTopicId` INTEGER NOT NULL, `sectionId` INTEGER NOT NULL, PRIMARY KEY(`discussionTopicId`, `sectionId`), FOREIGN KEY(`discussionTopicId`) REFERENCES `DiscussionTopicHeaderEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "discussionTopicId", + "columnName": "discussionTopicId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "discussionTopicId", + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionTopicHeaderEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionTopicId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "EnrollmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `role` TEXT NOT NULL, `type` TEXT NOT NULL, `courseId` INTEGER, `courseSectionId` INTEGER, `enrollmentState` TEXT, `userId` INTEGER NOT NULL, `computedCurrentScore` REAL, `computedFinalScore` REAL, `computedCurrentGrade` TEXT, `computedFinalGrade` TEXT, `multipleGradingPeriodsEnabled` INTEGER NOT NULL, `totalsForAllGradingPeriodsOption` INTEGER NOT NULL, `currentPeriodComputedCurrentScore` REAL, `currentPeriodComputedFinalScore` REAL, `currentPeriodComputedCurrentGrade` TEXT, `currentPeriodComputedFinalGrade` TEXT, `currentGradingPeriodId` INTEGER NOT NULL, `currentGradingPeriodTitle` TEXT, `associatedUserId` INTEGER NOT NULL, `lastActivityAt` INTEGER, `limitPrivilegesToCourseSection` INTEGER NOT NULL, `observedUserId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`observedUserId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseSectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "courseSectionId", + "columnName": "courseSectionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "computedCurrentScore", + "columnName": "computedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedFinalScore", + "columnName": "computedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "computedCurrentGrade", + "columnName": "computedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "computedFinalGrade", + "columnName": "computedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "multipleGradingPeriodsEnabled", + "columnName": "multipleGradingPeriodsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalsForAllGradingPeriodsOption", + "columnName": "totalsForAllGradingPeriodsOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPeriodComputedCurrentScore", + "columnName": "currentPeriodComputedCurrentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalScore", + "columnName": "currentPeriodComputedFinalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedCurrentGrade", + "columnName": "currentPeriodComputedCurrentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentPeriodComputedFinalGrade", + "columnName": "currentPeriodComputedFinalGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentGradingPeriodId", + "columnName": "currentGradingPeriodId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGradingPeriodTitle", + "columnName": "currentGradingPeriodTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "associatedUserId", + "columnName": "associatedUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivityAt", + "columnName": "lastActivityAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "limitPrivilegesToCourseSection", + "columnName": "limitPrivilegesToCourseSection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observedUserId", + "columnName": "observedUserId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "observedUserId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "SectionEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "courseSectionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileFolderEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `createdDate` INTEGER, `updatedDate` INTEGER, `unlockDate` INTEGER, `lockDate` INTEGER, `isLocked` INTEGER NOT NULL, `isHidden` INTEGER NOT NULL, `isLockedForUser` INTEGER NOT NULL, `isHiddenForUser` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `size` INTEGER NOT NULL, `contentType` TEXT, `url` TEXT, `displayName` TEXT, `thumbnailUrl` TEXT, `parentFolderId` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `filesCount` INTEGER NOT NULL, `position` INTEGER NOT NULL, `foldersCount` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `foldersUrl` TEXT, `filesUrl` TEXT, `fullName` TEXT, `forSubmissions` INTEGER NOT NULL, `canUpload` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedDate", + "columnName": "updatedDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unlockDate", + "columnName": "unlockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockDate", + "columnName": "lockDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLockedForUser", + "columnName": "isLockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHiddenForUser", + "columnName": "isHiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentFolderId", + "columnName": "parentFolderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesCount", + "columnName": "filesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "foldersCount", + "columnName": "foldersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "foldersUrl", + "columnName": "foldersUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesUrl", + "columnName": "filesUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forSubmissions", + "columnName": "forSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canUpload", + "columnName": "canUpload", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EditDashboardItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `name` TEXT NOT NULL, `isFavorite` INTEGER NOT NULL, `enrollmentState` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enrollmentState", + "columnName": "enrollmentState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ExternalToolAttributesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentId` INTEGER NOT NULL, `url` TEXT, `newTab` INTEGER NOT NULL, `resourceLinkid` TEXT, `contentId` INTEGER, PRIMARY KEY(`assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "newTab", + "columnName": "newTab", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceLinkid", + "columnName": "resourceLinkid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`enrollmentId` INTEGER NOT NULL, `htmlUrl` TEXT NOT NULL, `currentScore` REAL, `finalScore` REAL, `currentGrade` TEXT, `finalGrade` TEXT, PRIMARY KEY(`enrollmentId`), FOREIGN KEY(`enrollmentId`) REFERENCES `EnrollmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "enrollmentId", + "columnName": "enrollmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScore", + "columnName": "currentScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "finalScore", + "columnName": "finalScore", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "currentGrade", + "columnName": "currentGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "finalGrade", + "columnName": "finalGrade", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "enrollmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "EnrollmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "enrollmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "GradingPeriodEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `startDate` TEXT, `endDate` TEXT, `weight` REAL NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `description` TEXT, `avatarUrl` TEXT, `isPublic` INTEGER NOT NULL, `membersCount` INTEGER NOT NULL, `joinLevel` TEXT, `courseId` INTEGER NOT NULL, `accountId` INTEGER NOT NULL, `role` TEXT, `groupCategoryId` INTEGER NOT NULL, `storageQuotaMb` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `concluded` INTEGER NOT NULL, `canAccess` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinLevel", + "columnName": "joinLevel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupCategoryId", + "columnName": "groupCategoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storageQuotaMb", + "columnName": "storageQuotaMb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concluded", + "columnName": "concluded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canAccess", + "columnName": "canAccess", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GroupUserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LocalFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `createdDate` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdDate", + "columnName": "createdDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MasteryPathAssignmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `overrideId` INTEGER NOT NULL, `assignmentSetId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentSetId`) REFERENCES `AssignmentSetEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "overrideId", + "columnName": "overrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentSetId", + "columnName": "assignmentSetId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentSetEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentSetId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MasteryPathEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `selectedSetId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectedSetId", + "columnName": "selectedSetId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleContentDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pointsPossible` TEXT, `dueAt` TEXT, `unlockAt` TEXT, `lockAt` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `ModuleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `moduleId` INTEGER NOT NULL, `position` INTEGER NOT NULL, `title` TEXT, `indent` INTEGER NOT NULL, `type` TEXT, `htmlUrl` TEXT, `url` TEXT, `published` INTEGER, `contentId` INTEGER NOT NULL, `externalUrl` TEXT, `pageUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentId", + "columnName": "contentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageUrl", + "columnName": "pageUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleObjectEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `name` TEXT, `unlockAt` TEXT, `sequentialProgress` INTEGER NOT NULL, `prerequisiteIds` TEXT, `state` TEXT, `completedAt` TEXT, `published` INTEGER, `itemCount` INTEGER NOT NULL, `itemsUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sequentialProgress", + "columnName": "sequentialProgress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prerequisiteIds", + "columnName": "prerequisiteIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "completedAt", + "columnName": "completedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemCount", + "columnName": "itemCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsUrl", + "columnName": "itemsUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "NeedsGradingCountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sectionId` INTEGER NOT NULL, `needsGradingCount` INTEGER NOT NULL, PRIMARY KEY(`sectionId`), FOREIGN KEY(`sectionId`) REFERENCES `SectionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "needsGradingCount", + "columnName": "needsGradingCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sectionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SectionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `createdAt` INTEGER, `updatedAt` INTEGER, `hideFromStudents` INTEGER NOT NULL, `status` TEXT, `body` TEXT, `frontPage` INTEGER NOT NULL, `published` INTEGER NOT NULL, `editingRoles` TEXT, `htmlUrl` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideFromStudents", + "columnName": "hideFromStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "frontPage", + "columnName": "frontPage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editingRoles", + "columnName": "editingRoles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "PlannerOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `plannableType` TEXT NOT NULL, `plannableId` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, `markedComplete` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plannableType", + "columnName": "plannableType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "plannableId", + "columnName": "plannableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "markedComplete", + "columnName": "markedComplete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RemoteFileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `folderId` INTEGER NOT NULL, `displayName` TEXT, `fileName` TEXT, `contentType` TEXT, `url` TEXT, `size` INTEGER NOT NULL, `createdAt` TEXT, `updatedAt` TEXT, `unlockAt` TEXT, `locked` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `lockAt` TEXT, `hiddenForUser` INTEGER NOT NULL, `thumbnailUrl` TEXT, `modifiedAt` TEXT, `lockedForUser` INTEGER NOT NULL, `previewUrl` TEXT, `lockExplanation` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hiddenForUser", + "columnName": "hiddenForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "modifiedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "RubricCriterionAssessmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `assignmentId` INTEGER NOT NULL, `ratingId` TEXT, `points` REAL, `comments` TEXT, PRIMARY KEY(`id`, `assignmentId`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingId", + "columnName": "ratingId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "comments", + "columnName": "comments", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "assignmentId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `criterionUseRange` INTEGER NOT NULL, `ignoreForScoring` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "criterionUseRange", + "columnName": "criterionUseRange", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoreForScoring", + "columnName": "ignoreForScoring", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricCriterionRatingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `description` TEXT, `longDescription` TEXT, `points` REAL NOT NULL, `rubricCriterionId` TEXT NOT NULL, PRIMARY KEY(`id`, `rubricCriterionId`), FOREIGN KEY(`rubricCriterionId`) REFERENCES `RubricCriterionEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longDescription", + "columnName": "longDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rubricCriterionId", + "columnName": "rubricCriterionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "rubricCriterionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "RubricCriterionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "rubricCriterionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "RubricSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `pointsPossible` REAL NOT NULL, `title` TEXT NOT NULL, `isReusable` INTEGER NOT NULL, `isPublic` INTEGER NOT NULL, `isReadOnly` INTEGER NOT NULL, `freeFormCriterionComments` INTEGER NOT NULL, `hideScoreTotal` INTEGER NOT NULL, `hidePoints` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReusable", + "columnName": "isReusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPublic", + "columnName": "isPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "isReadOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "freeFormCriterionComments", + "columnName": "freeFormCriterionComments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideScoreTotal", + "columnName": "hideScoreTotal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidePoints", + "columnName": "hidePoints", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemAssignmentOverrideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`assignmentOverrideId` INTEGER NOT NULL, `scheduleItemId` TEXT NOT NULL, PRIMARY KEY(`assignmentOverrideId`, `scheduleItemId`), FOREIGN KEY(`assignmentOverrideId`) REFERENCES `AssignmentOverrideEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`scheduleItemId`) REFERENCES `ScheduleItemEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "assignmentOverrideId", + "columnName": "assignmentOverrideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduleItemId", + "columnName": "scheduleItemId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "assignmentOverrideId", + "scheduleItemId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AssignmentOverrideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentOverrideId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "ScheduleItemEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "scheduleItemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ScheduleItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `description` TEXT, `startAt` TEXT, `endAt` TEXT, `isAllDay` INTEGER NOT NULL, `allDayAt` TEXT, `locationAddress` TEXT, `locationName` TEXT, `htmlUrl` TEXT, `contextCode` TEXT, `effectiveContextCode` TEXT, `isHidden` INTEGER NOT NULL, `importantDates` INTEGER NOT NULL, `assignmentId` INTEGER, `type` TEXT NOT NULL, `itemType` TEXT, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allDayAt", + "columnName": "allDayAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationAddress", + "columnName": "locationAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextCode", + "columnName": "contextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effectiveContextCode", + "columnName": "effectiveContextCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "importantDates", + "columnName": "importantDates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemType", + "columnName": "itemType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `courseId` INTEGER, `startAt` TEXT, `endAt` TEXT, `totalStudents` INTEGER NOT NULL, `restrictEnrollmentsToSectionDates` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "totalStudents", + "columnName": "totalStudents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "restrictEnrollmentsToSectionDates", + "columnName": "restrictEnrollmentsToSectionDates", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionDiscussionEntryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`submissionId` INTEGER NOT NULL, `discussionEntryId` INTEGER NOT NULL, PRIMARY KEY(`submissionId`, `discussionEntryId`), FOREIGN KEY(`discussionEntryId`) REFERENCES `DiscussionEntryEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "discussionEntryId", + "columnName": "discussionEntryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "submissionId", + "discussionEntryId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "DiscussionEntryEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "discussionEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SubmissionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `grade` TEXT, `score` REAL NOT NULL, `attempt` INTEGER NOT NULL, `submittedAt` INTEGER, `commentCreated` INTEGER, `mediaContentType` TEXT, `mediaCommentUrl` TEXT, `mediaCommentDisplay` TEXT, `body` TEXT, `isGradeMatchesCurrentSubmission` INTEGER NOT NULL, `workflowState` TEXT, `submissionType` TEXT, `previewUrl` TEXT, `url` TEXT, `late` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `missing` INTEGER NOT NULL, `mediaCommentId` TEXT, `assignmentId` INTEGER NOT NULL, `userId` INTEGER, `graderId` INTEGER, `groupId` INTEGER, `pointsDeducted` REAL, `enteredScore` REAL NOT NULL, `enteredGrade` TEXT, `postedAt` INTEGER, `gradingPeriodId` INTEGER, PRIMARY KEY(`id`, `attempt`), FOREIGN KEY(`groupId`) REFERENCES `GroupEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submittedAt", + "columnName": "submittedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "commentCreated", + "columnName": "commentCreated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaContentType", + "columnName": "mediaContentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentUrl", + "columnName": "mediaCommentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaCommentDisplay", + "columnName": "mediaCommentDisplay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGradeMatchesCurrentSubmission", + "columnName": "isGradeMatchesCurrentSubmission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workflowState", + "columnName": "workflowState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionType", + "columnName": "submissionType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "late", + "columnName": "late", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missing", + "columnName": "missing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "graderId", + "columnName": "graderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pointsDeducted", + "columnName": "pointsDeducted", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "enteredScore", + "columnName": "enteredScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "enteredGrade", + "columnName": "enteredGrade", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "gradingPeriodId", + "columnName": "gradingPeriodId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "attempt" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GroupEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserEntity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `autoSyncEnabled` INTEGER NOT NULL, `syncFrequency` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoSyncEnabled", + "columnName": "autoSyncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncFrequency", + "columnName": "syncFrequency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TabEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `label` TEXT, `type` TEXT NOT NULL, `htmlUrl` TEXT, `externalUrl` TEXT, `visibility` TEXT NOT NULL, `isHidden` INTEGER NOT NULL, `position` INTEGER NOT NULL, `ltiUrl` TEXT NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`, `courseId`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalUrl", + "columnName": "externalUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHidden", + "columnName": "isHidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ltiUrl", + "columnName": "ltiUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "courseId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TermEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `startAt` TEXT, `endAt` TEXT, `isGroupTerm` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startAt", + "columnName": "startAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endAt", + "columnName": "endAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isGroupTerm", + "columnName": "isGroupTerm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserCalendarEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ics` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ics", + "columnName": "ics", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortName` TEXT, `loginId` TEXT, `avatarUrl` TEXT, `primaryEmail` TEXT, `email` TEXT, `sortableName` TEXT, `bio` TEXT, `enrollmentIndex` INTEGER NOT NULL, `lastLogin` TEXT, `locale` TEXT, `effective_locale` TEXT, `pronouns` TEXT, `k5User` INTEGER NOT NULL, `rootAccount` TEXT, `isFakeStudent` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "shortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginId", + "columnName": "loginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "primaryEmail", + "columnName": "primaryEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortableName", + "columnName": "sortableName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bio", + "columnName": "bio", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enrollmentIndex", + "columnName": "enrollmentIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastLogin", + "columnName": "lastLogin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "effective_locale", + "columnName": "effective_locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "k5User", + "columnName": "k5User", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rootAccount", + "columnName": "rootAccount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFakeStudent", + "columnName": "isFakeStudent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "QuizEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT, `mobileUrl` TEXT, `htmlUrl` TEXT, `description` TEXT, `quizType` TEXT, `assignmentGroupId` INTEGER NOT NULL, `allowedAttempts` INTEGER NOT NULL, `questionCount` INTEGER NOT NULL, `pointsPossible` TEXT, `isLockQuestionsAfterAnswering` INTEGER NOT NULL, `dueAt` TEXT, `timeLimit` INTEGER NOT NULL, `shuffleAnswers` INTEGER NOT NULL, `showCorrectAnswers` INTEGER NOT NULL, `scoringPolicy` TEXT, `accessCode` TEXT, `ipFilter` TEXT, `lockedForUser` INTEGER NOT NULL, `lockExplanation` TEXT, `hideResults` TEXT, `showCorrectAnswersAt` TEXT, `hideCorrectAnswersAt` TEXT, `unlockAt` TEXT, `oneTimeResults` INTEGER NOT NULL, `lockAt` TEXT, `questionTypes` TEXT NOT NULL, `hasAccessCode` INTEGER NOT NULL, `oneQuestionAtATime` INTEGER NOT NULL, `requireLockdownBrowser` INTEGER NOT NULL, `requireLockdownBrowserForResults` INTEGER NOT NULL, `allowAnonymousSubmissions` INTEGER NOT NULL, `published` INTEGER NOT NULL, `assignmentId` INTEGER NOT NULL, `isOnlyVisibleToOverrides` INTEGER NOT NULL, `unpublishable` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mobileUrl", + "columnName": "mobileUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quizType", + "columnName": "quizType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "assignmentGroupId", + "columnName": "assignmentGroupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowedAttempts", + "columnName": "allowedAttempts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionCount", + "columnName": "questionCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pointsPossible", + "columnName": "pointsPossible", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLockQuestionsAfterAnswering", + "columnName": "isLockQuestionsAfterAnswering", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dueAt", + "columnName": "dueAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeLimit", + "columnName": "timeLimit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleAnswers", + "columnName": "shuffleAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showCorrectAnswers", + "columnName": "showCorrectAnswers", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scoringPolicy", + "columnName": "scoringPolicy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accessCode", + "columnName": "accessCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ipFilter", + "columnName": "ipFilter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedForUser", + "columnName": "lockedForUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockExplanation", + "columnName": "lockExplanation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideResults", + "columnName": "hideResults", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showCorrectAnswersAt", + "columnName": "showCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideCorrectAnswersAt", + "columnName": "hideCorrectAnswersAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTimeResults", + "columnName": "oneTimeResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockAt", + "columnName": "lockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionTypes", + "columnName": "questionTypes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasAccessCode", + "columnName": "hasAccessCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneQuestionAtATime", + "columnName": "oneQuestionAtATime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowser", + "columnName": "requireLockdownBrowser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireLockdownBrowserForResults", + "columnName": "requireLockdownBrowserForResults", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "allowAnonymousSubmissions", + "columnName": "allowAnonymousSubmissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "published", + "columnName": "published", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlyVisibleToOverrides", + "columnName": "isOnlyVisibleToOverrides", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unpublishable", + "columnName": "unpublishable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockInfoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `modulePrerequisiteNames` TEXT, `unlockAt` TEXT, `lockedModuleId` INTEGER, `assignmentId` INTEGER, `moduleId` INTEGER, `pageId` INTEGER, FOREIGN KEY(`moduleId`) REFERENCES `ModuleContentDetailsEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`assignmentId`) REFERENCES `AssignmentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`pageId`) REFERENCES `PageEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modulePrerequisiteNames", + "columnName": "modulePrerequisiteNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignmentId", + "columnName": "assignmentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_LockInfoEntity_lockedModuleId", + "unique": true, + "columnNames": [ + "lockedModuleId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LockInfoEntity_lockedModuleId` ON `${TABLE_NAME}` (`lockedModuleId`)" + } + ], + "foreignKeys": [ + { + "table": "ModuleContentDetailsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "AssignmentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "assignmentId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "PageEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "LockedModuleEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contextId` INTEGER NOT NULL, `contextType` TEXT, `name` TEXT, `unlockAt` TEXT, `isRequireSequentialProgress` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `LockInfoEntity`(`lockedModuleId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unlockAt", + "columnName": "unlockAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isRequireSequentialProgress", + "columnName": "isRequireSequentialProgress", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockInfoEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "lockedModuleId" + ] + } + ] + }, + { + "tableName": "ModuleNameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `lockedModuleId` INTEGER NOT NULL, FOREIGN KEY(`lockedModuleId`) REFERENCES `LockedModuleEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockedModuleId", + "columnName": "lockedModuleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "LockedModuleEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockedModuleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ModuleCompletionRequirementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT, `minScore` REAL NOT NULL, `maxScore` REAL NOT NULL, `completed` INTEGER, `moduleId` INTEGER NOT NULL, FOREIGN KEY(`moduleId`) REFERENCES `ModuleObjectEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "minScore", + "columnName": "minScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxScore", + "columnName": "maxScore", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "moduleId", + "columnName": "moduleId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ModuleObjectEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "moduleId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FileSyncSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `fileName` TEXT, `courseId` INTEGER NOT NULL, `url` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncSettingsEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncSettingsEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + }, + { + "tableName": "ConferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `courseId` INTEGER NOT NULL, `conferenceKey` TEXT, `conferenceType` TEXT, `description` TEXT, `duration` INTEGER NOT NULL, `endedAt` INTEGER, `hasAdvancedSettings` INTEGER NOT NULL, `joinUrl` TEXT, `longRunning` INTEGER NOT NULL, `startedAt` INTEGER, `title` TEXT, `url` TEXT, `contextType` TEXT NOT NULL, `contextId` INTEGER NOT NULL, `record` INTEGER, `users` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`courseId`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conferenceKey", + "columnName": "conferenceKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "conferenceType", + "columnName": "conferenceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAdvancedSettings", + "columnName": "hasAdvancedSettings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "joinUrl", + "columnName": "joinUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longRunning", + "columnName": "longRunning", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contextType", + "columnName": "contextType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextId", + "columnName": "contextId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "record", + "columnName": "record", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ConferenceRecordingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recordingId` TEXT NOT NULL, `conferenceId` INTEGER NOT NULL, `createdAtMillis` INTEGER NOT NULL, `durationMinutes` INTEGER NOT NULL, `playbackUrl` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`recordingId`), FOREIGN KEY(`conferenceId`) REFERENCES `ConferenceEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recordingId", + "columnName": "recordingId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conferenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtMillis", + "columnName": "createdAtMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationMinutes", + "columnName": "durationMinutes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playbackUrl", + "columnName": "playbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recordingId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "ConferenceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "conferenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CourseFeaturesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `features` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `CourseEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "AttachmentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `contentType` TEXT, `filename` TEXT, `displayName` TEXT, `url` TEXT, `thumbnailUrl` TEXT, `previewUrl` TEXT, `createdAt` INTEGER, `size` INTEGER NOT NULL, `workerId` TEXT, `submissionCommentId` INTEGER, `submissionId` INTEGER, `attempt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionCommentId`) REFERENCES `SubmissionCommentEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "previewUrl", + "columnName": "previewUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submissionCommentId", + "columnName": "submissionCommentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attempt", + "columnName": "attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionCommentEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionCommentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "MediaCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mediaId` TEXT NOT NULL, `submissionId` INTEGER NOT NULL, `attemptId` INTEGER NOT NULL, `displayName` TEXT, `url` TEXT, `mediaType` TEXT, `contentType` TEXT, PRIMARY KEY(`mediaId`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mediaId", + "columnName": "mediaId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mediaType", + "columnName": "mediaType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mediaId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "AuthorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `displayName` TEXT, `avatarImageUrl` TEXT, `htmlUrl` TEXT, `pronouns` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarImageUrl", + "columnName": "avatarImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "htmlUrl", + "columnName": "htmlUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SubmissionCommentEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `authorId` INTEGER NOT NULL, `authorName` TEXT, `authorPronouns` TEXT, `comment` TEXT, `createdAt` INTEGER, `mediaCommentId` TEXT, `attemptId` INTEGER, `submissionId` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`submissionId`, `attemptId`) REFERENCES `SubmissionEntity`(`id`, `attempt`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorId", + "columnName": "authorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPronouns", + "columnName": "authorPronouns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mediaCommentId", + "columnName": "mediaCommentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attemptId", + "columnName": "attemptId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "submissionId", + "columnName": "submissionId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "SubmissionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "submissionId", + "attemptId" + ], + "referencedColumns": [ + "id", + "attempt" + ] + } + ] + }, + { + "tableName": "DiscussionTopicEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `unreadEntries` TEXT NOT NULL, `participantIds` TEXT NOT NULL, `viewIds` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadEntries", + "columnName": "unreadEntries", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantIds", + "columnName": "participantIds", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewIds", + "columnName": "viewIds", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CourseSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`courseId` INTEGER NOT NULL, `workerId` TEXT NOT NULL, `courseName` TEXT NOT NULL, `tabs` TEXT NOT NULL, `additionalFilesStarted` INTEGER NOT NULL, `progressState` TEXT NOT NULL, PRIMARY KEY(`courseId`))", + "fields": [ + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseName", + "columnName": "courseName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabs", + "columnName": "tabs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalFilesStarted", + "columnName": "additionalFilesStarted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "courseId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FileSyncProgressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`workerId` TEXT NOT NULL, `courseId` INTEGER NOT NULL, `fileName` TEXT NOT NULL, `progress` INTEGER NOT NULL, `fileSize` INTEGER NOT NULL, `additionalFile` INTEGER NOT NULL, `progressState` TEXT NOT NULL, `fileId` INTEGER NOT NULL, PRIMARY KEY(`workerId`), FOREIGN KEY(`courseId`) REFERENCES `CourseSyncProgressEntity`(`courseId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "workerId", + "columnName": "workerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "courseId", + "columnName": "courseId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileSize", + "columnName": "fileSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalFile", + "columnName": "additionalFile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressState", + "columnName": "progressState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "fileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "workerId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CourseSyncProgressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "courseId" + ], + "referencedColumns": [ + "courseId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f792290082e6cf2f70533d168ec4901b')" + ] + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDaoTest.kt index 652c98bb0c..0b7557d62d 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDaoTest.kt @@ -21,8 +21,7 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.pandautils.room.appdatabase.AppDatabase -import com.instructure.pandautils.room.common.daos.AttachmentDao -import com.instructure.pandautils.room.common.entities.AttachmentEntity +import com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After @@ -30,7 +29,7 @@ import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import java.util.Date +import java.util.* @ExperimentalCoroutinesApi @RunWith(AndroidJUnit4::class) @@ -47,7 +46,7 @@ class AttachmentDaoTest { } @After - fun tearDoown() { + fun tearDown() { db.close() } @@ -81,18 +80,4 @@ class AttachmentDaoTest { Assert.assertEquals(0, result!!.size) } - - @Test - fun testFindBySubmissionId() = runTest { - val attachmentEntity = AttachmentEntity( - id = 1, contentType = "image/jpg", filename = "image.jpg", displayName = "File", url = "file.com", - createdAt = Date(), size = 10000, workerId = "123", submissionCommentId = 123, submissionId = 1 - ) - val attachmentEntity2 = attachmentEntity.copy(id = 2, workerId = "124", filename = "image2.jpg", submissionId = 2) - attachmentDao.insertAll(listOf(attachmentEntity, attachmentEntity2)) - - val result = attachmentDao.findBySubmissionId(1) - - Assert.assertEquals(listOf(attachmentEntity), result) - } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/DashboardFileUploadDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/DashboardFileUploadDaoTest.kt index 229d4b8cd5..1bb278ef01 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/DashboardFileUploadDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/DashboardFileUploadDaoTest.kt @@ -18,7 +18,6 @@ package com.instructure.pandautils.room.appdatabase.daos import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,11 +25,7 @@ import com.instructure.pandautils.room.appdatabase.AppDatabase import com.instructure.pandautils.room.appdatabase.entities.DashboardFileUploadEntity import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Assert -import org.junit.Before -import org.junit.Rule -import org.junit.Test +import org.junit.* import org.junit.runner.RunWith @ExperimentalCoroutinesApi @@ -51,7 +46,7 @@ class DashboardFileUploadDaoTest { } @After - fun tearDoown() { + fun tearDown() { db.close() } diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/FileUploadInputDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/FileUploadInputDaoTest.kt index 5b98447cdb..94bce7d1e4 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/FileUploadInputDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/FileUploadInputDaoTest.kt @@ -45,7 +45,7 @@ class FileUploadInputDaoTest { } @After - fun tearDoown() { + fun tearDown() { db.close() } diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/PendingSubmissionCommentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/PendingSubmissionCommentDaoTest.kt index 3efdddd201..3b8753a4cd 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/PendingSubmissionCommentDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/PendingSubmissionCommentDaoTest.kt @@ -49,7 +49,7 @@ class PendingSubmissionCommentDaoTest { } @After - fun tearDoown() { + fun tearDown() { db.close() } diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDaoTest.kt index 9b75a0be9d..0a2aa74260 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDaoTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDaoTest.kt @@ -21,14 +21,10 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.instructure.pandautils.room.appdatabase.AppDatabase -import com.instructure.pandautils.room.common.daos.AttachmentDao -import com.instructure.pandautils.room.common.daos.AuthorDao -import com.instructure.pandautils.room.common.daos.MediaCommentDao -import com.instructure.pandautils.room.common.daos.SubmissionCommentDao -import com.instructure.pandautils.room.common.entities.AttachmentEntity -import com.instructure.pandautils.room.common.entities.AuthorEntity -import com.instructure.pandautils.room.common.entities.MediaCommentEntity -import com.instructure.pandautils.room.common.entities.SubmissionCommentEntity +import com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity +import com.instructure.pandautils.room.appdatabase.entities.AuthorEntity +import com.instructure.pandautils.room.appdatabase.entities.MediaCommentEntity +import com.instructure.pandautils.room.appdatabase.entities.SubmissionCommentEntity import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After @@ -60,7 +56,7 @@ class SubmissionCommentDaoTest { } @After - fun tearDoown() { + fun tearDown() { db.close() } @@ -95,26 +91,4 @@ class SubmissionCommentDaoTest { Assert.assertEquals("Obi-Wan", result.author!!.displayName) Assert.assertEquals("Order 66", result.mediaComment!!.displayName) } - - @Test - fun testFindBySubmissionId() = runTest { - val submissionComment = SubmissionCommentEntity( - id = 1, - comment = "These are the droids you are looking for", - authorId = 1, - mediaCommentId = "66", - submissionId = 1 - ) - val submissionComment2 = SubmissionCommentEntity( - id = 2, - comment = "These are not the droids you are looking for", - submissionId = 2 - ) - submissionCommentDao.insertAll(listOf(submissionComment, submissionComment2)) - - val result = submissionCommentDao.findBySubmissionId(1) - - Assert.assertEquals(1, result.size) - Assert.assertEquals(submissionComment, result.first().submissionComment) - } } \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentDaoTest.kt new file mode 100644 index 0000000000..8cb0702d27 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentDaoTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AssignmentDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var assignmentDao: AssignmentDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + assignmentDao = db.assignmentDao() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = + AssignmentEntity(Assignment(id = 1L, name = "assignmentEntity", assignmentGroupId = 1L, courseId = 1L), null, null, null, null) + val updated = assignmentEntity.copy(name = "updated") + + assignmentDao.insert(assignmentEntity) + assignmentDao.insert(updated) + + val result = assignmentDao.findById(1L) + + assertEquals(updated.name, result?.name) + } + + @Test + fun testFindById() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = + AssignmentEntity(Assignment(id = 1L, assignmentGroupId = 1L, courseId = 1L), null, null, null, null) + val assignmentEntity2 = + AssignmentEntity(Assignment(id = 2L, assignmentGroupId = 1L, courseId = 1L), null, null, null, null) + assignmentDao.insert(assignmentEntity) + assignmentDao.insert(assignmentEntity2) + + val result = assignmentDao.findById(2L) + + assertEquals(assignmentEntity2.id, result?.id) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentGroupForeignKey() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val assignmentEntity = + AssignmentEntity(Assignment(id = 1L, assignmentGroupId = 1L, courseId = 1L), null, null, null, null) + assignmentDao.insert(assignmentEntity) + } + + @Test + fun testCourseCascade() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = + AssignmentEntity(Assignment(id = 1L, assignmentGroupId = 1L, courseId = 1L), null, null, null, null) + assignmentDao.insert(assignmentEntity) + + courseDao.delete(courseEntity) + + val result = assignmentDao.findById(1L) + + assertNull(result) + } + + @Test + fun testAssignmentGroupCascade() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val assignmentGroupEntity = AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L) + assignmentGroupDao.insert(assignmentGroupEntity) + + val assignmentEntity = AssignmentEntity( + Assignment(id = 1L, assignmentGroupId = 1L, courseId = 1L), + null, null, null, null + ) + assignmentDao.insertOrUpdate(assignmentEntity) + + assignmentGroupDao.delete(assignmentGroupEntity) + + val result = assignmentDao.findById(1L) + + assertNull(result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentGroupDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentGroupDaoTest.kt new file mode 100644 index 0000000000..e78ed77841 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentGroupDaoTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AssignmentGroupDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + assignmentGroupDao = db.assignmentGroupDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1))) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + val assignmentGroupEntity = AssignmentGroupEntity( + id = 1L, + name = "Name 1", + position = 0, + groupWeight = 0.0, + rules = null, + 1L + ) + + val assignmentGroupEntity2 = assignmentGroupEntity.copy(id = 2L, name = "Name 2") + + assignmentGroupDao.insert(assignmentGroupEntity) + assignmentGroupDao.insert(assignmentGroupEntity2) + + val result = assignmentGroupDao.findById(1L) + + Assert.assertEquals(assignmentGroupEntity, result) + } + + @Test + fun testFindByIdReturnsNullIfNotFound() = runTest { + val assignmentGroupEntity = AssignmentGroupEntity( + id = 1L, + name = "Name 1", + position = 0, + groupWeight = 0.0, + rules = null, + 1L + ) + + val assignmentGroupEntity2 = assignmentGroupEntity.copy(id = 2L, name = "Name 2") + + assignmentGroupDao.insert(assignmentGroupEntity) + assignmentGroupDao.insert(assignmentGroupEntity2) + + val result = assignmentGroupDao.findById(3L) + + Assert.assertNull(result) + } + + @Test + fun testInsertReplace() = runTest { + val assignmentGroupEntity = AssignmentGroupEntity( + id = 1L, + name = "Name 1", + position = 0, + groupWeight = 0.0, + rules = null, + 1L + ) + + assignmentGroupDao.insert(assignmentGroupEntity) + + val updated = assignmentGroupEntity.copy(position = 1) + assignmentGroupDao.insert(updated) + + val result = assignmentGroupDao.findById(1L) + + Assert.assertEquals(updated, result) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + val assignmentGroupEntity = AssignmentGroupEntity( + id = 1L, + name = "Name 1", + position = 0, + groupWeight = 0.0, + rules = null, + 1L + ) + assignmentGroupDao.insert(assignmentGroupEntity) + + val result = assignmentGroupDao.findById(1L) + + Assert.assertEquals(assignmentGroupEntity, result) + + assignmentGroupDao.deleteAllByCourseId(1L) + + val deletedResult = assignmentGroupDao.findById(1L) + + Assert.assertNull(deletedResult) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentOverrideDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentOverrideDaoTest.kt new file mode 100644 index 0000000000..b33383f989 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentOverrideDaoTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.AssignmentOverride +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.AssignmentOverrideEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AssignmentOverrideDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var assignmentOverrideDao: AssignmentOverrideDao + private lateinit var assignmentDao: AssignmentDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + assignmentOverrideDao = db.assignmentOverrideDao() + assignmentDao = db.assignmentDao() + db.courseDao().insert(CourseEntity(Course(id = 1L))) + db.assignmentGroupDao().insert(AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L)) + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + val assignmentEntity = AssignmentEntity(Assignment(1L, courseId = 1L, assignmentGroupId = 1L), null, null, null, null) + assignmentDao.insert(assignmentEntity) + + val assignmentOverrideEntity = + AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L, title = "assignment override")) + val updated = assignmentOverrideEntity.copy(title = "updated") + + assignmentOverrideDao.insert(assignmentOverrideEntity) + assignmentOverrideDao.insert(updated) + + val result = assignmentOverrideDao.findByIds(listOf(1L)) + + assertEquals(listOf(updated), result) + } + + @Test + fun testFindByIds() = runTest { + val assignmentEntity = AssignmentEntity(Assignment(1L, courseId = 1L, assignmentGroupId = 1L), null, null, null, null) + assignmentDao.insert(assignmentEntity) + + val assignmentOverrideEntity = + AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L, title = "assignment override")) + val assignmentOverrideEntity2 = + AssignmentOverrideEntity(AssignmentOverride(id = 2L, assignmentId = 1L, title = "assignment override")) + val assignmentOverrideEntity3 = + AssignmentOverrideEntity(AssignmentOverride(id = 3L, assignmentId = 1L, title = "assignment override")) + + assignmentOverrideDao.insert(assignmentOverrideEntity) + assignmentOverrideDao.insert(assignmentOverrideEntity2) + assignmentOverrideDao.insert(assignmentOverrideEntity3) + + val result = assignmentOverrideDao.findByIds(listOf(2L, 3L)) + + assertEquals(listOf(assignmentOverrideEntity2, assignmentOverrideEntity3), result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentForeignKey() = runTest { + val assignmentOverrideEntity = + AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L, title = "assignment override")) + + assignmentOverrideDao.insert(assignmentOverrideEntity) + } + + @Test + fun testAssignmentCascade() = runTest { + val assignmentEntity = AssignmentEntity(Assignment(1L, courseId = 1L, assignmentGroupId = 1L), null, null, null, null) + assignmentDao.insert(assignmentEntity) + + val assignmentOverrideEntity = + AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L, title = "assignment override")) + + assignmentOverrideDao.insert(assignmentOverrideEntity) + + assignmentDao.delete(assignmentEntity) + + val result = assignmentOverrideDao.findByIds(listOf(1L)) + + assert(result.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentRubricCriterionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentRubricCriterionDaoTest.kt new file mode 100644 index 0000000000..329c1ae259 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentRubricCriterionDaoTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.AssignmentRubricCriterionEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AssignmentRubricCriterionDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var assignmentRubricCriterionDao: AssignmentRubricCriterionDao + private lateinit var assignmentDao: AssignmentDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + assignmentRubricCriterionDao = db.assignmentRubricCriterionDao() + assignmentDao = db.assignmentDao() + + runBlocking { + db.courseDao().insert(CourseEntity(Course(id = 1L))) + db.assignmentGroupDao().insert(AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L)) + assignmentDao.insert( + AssignmentEntity( + Assignment(id = 1L, courseId = 1L, assignmentGroupId = 1L), + null, + null, + null, + null + ) + ) + assignmentDao.insert( + AssignmentEntity( + Assignment(id = 2L, courseId = 1L, assignmentGroupId = 1L), + null, + null, + null, + null + ) + ) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByAssignmentId() = runTest { + val assignmentRubricCriterionEntity = AssignmentRubricCriterionEntity(1, "1") + val assignmentRubricCriterionEntity2 = AssignmentRubricCriterionEntity(2, "2") + assignmentRubricCriterionDao.insert(assignmentRubricCriterionEntity) + assignmentRubricCriterionDao.insert(assignmentRubricCriterionEntity2) + + val result = assignmentRubricCriterionDao.findByAssignmentId(1) + + Assert.assertEquals(listOf(assignmentRubricCriterionEntity), result) + } + + @Test + fun testAssignmentCascade() = runTest { + val assignmentRubricCriterionEntity = AssignmentRubricCriterionEntity(1, "1") + assignmentRubricCriterionDao.insert(assignmentRubricCriterionEntity) + + assignmentDao.delete( + AssignmentEntity( + Assignment(id = 1L, courseId = 1L, assignmentGroupId = 1L), + null, + null, + null, + null + ) + ) + + val result = assignmentRubricCriterionDao.findByAssignmentId(1) + + assert(result.isEmpty()) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentSetDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentSetDaoTest.kt new file mode 100644 index 0000000000..7f3d976729 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AssignmentSetDaoTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.AssignmentSet +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.MasteryPath +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AssignmentSetDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var assignmentSetDao: AssignmentSetDao + private lateinit var masteryPathDao: MasteryPathDao + private lateinit var moduleItemDao: ModuleItemDao + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + assignmentSetDao = db.assignmentSetDao() + masteryPathDao = db.masteryPathDao() + moduleItemDao = db.moduleItemDao() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByMasteryPathId() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 2), 1 )) + + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 1)) + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 2)) + + val entities = listOf( + AssignmentSetEntity(AssignmentSet(id = 1, createdAt = "2020-01-01T00:00:00Z"), 1), + AssignmentSetEntity(AssignmentSet(id = 2, createdAt = "2021-01-01T00:00:00Z"), 2), + AssignmentSetEntity(AssignmentSet(id = 3, createdAt = "2022-01-01T00:00:00Z"), 1), + ) + entities.forEach { + assignmentSetDao.insert(it) + } + + val result = assignmentSetDao.findByMasteryPathId(1) + + Assert.assertEquals(2, result.size) + Assert.assertEquals(1, result[0].id) + Assert.assertEquals(3, result[1].id) + } + + @Test(expected = SQLiteConstraintException::class) + fun testMasteryPathForeignKeyRequired() = runTest { + assignmentSetDao.insert( + AssignmentSetEntity( + AssignmentSet( + id = 1, + createdAt = "2020-01-01T00:00:00Z" + ), 1 + ) + ) + } + + @Test + fun testCascadeWhenMasteryPathDeleted() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 1)) + + val entities = listOf( + AssignmentSetEntity(AssignmentSet(id = 1, createdAt = "2020-01-01T00:00:00Z"), 1), + AssignmentSetEntity(AssignmentSet(id = 3, createdAt = "2022-01-01T00:00:00Z"), 1), + ) + entities.forEach { + assignmentSetDao.insert(it) + } + + val result = assignmentSetDao.findByMasteryPathId(1) + Assert.assertEquals(2, result.size) + + masteryPathDao.delete(MasteryPathEntity(MasteryPath(), 1)) + val resultAfterDelete = assignmentSetDao.findByMasteryPathId(1) + Assert.assertEquals(0, resultAfterDelete.size) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AttachmentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AttachmentDaoTest.kt new file mode 100644 index 0000000000..795f1e5589 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/AttachmentDaoTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class AttachmentDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var courseDao: CourseDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var submissionDao: SubmissionDao + private lateinit var submissionCommentDao: SubmissionCommentDao + private lateinit var attachmentDao: AttachmentDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseDao = db.courseDao() + assignmentGroupDao = db.assignmentGroupDao() + assignmentDao = db.assignmentDao() + submissionDao = db.submissionDao() + submissionCommentDao = db.submissionCommentDao() + attachmentDao = db.attachmentDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + submissionDao.insert(SubmissionEntity(Submission(1L, attempt = 1L, assignmentId = 1L), null, null)) + submissionCommentDao.insert(SubmissionCommentEntity(SubmissionComment(1L), 1L, 1L)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun insertAndFindingByParentId() = runTest { + val attachmentEntity = AttachmentEntity( + id = 1, contentType = "image/jpg", filename = "image.jpg", displayName = "File", + url = "file.com", createdAt = Date(), size = 10000, workerId = "123", submissionCommentId = 1L + ) + + val attachmentEntity2 = attachmentEntity.copy(id = 2, workerId = "124", filename = "image2.jpg") + + attachmentDao.insertAll(listOf(attachmentEntity, attachmentEntity2)) + val result = attachmentDao.findByParentId("123") + Assert.assertEquals(1, result!!.size) + Assert.assertEquals(attachmentEntity, result.first()) + } + + @Test + fun dontReturnAnyItemIfEntitiesAreDeleted() = runTest { + val attachmentEntity = AttachmentEntity( + id = 1, contentType = "image/jpg", filename = "image.jpg", displayName = "File", + url = "file.com", createdAt = Date(), size = 10000, workerId = "123", submissionCommentId = 1L + ) + + val attachmentEntity2 = attachmentEntity.copy(id = 2, workerId = "124", filename = "image2.jpg") + + attachmentDao.insertAll(listOf(attachmentEntity, attachmentEntity2)) + attachmentDao.deleteAll(listOf(attachmentEntity, attachmentEntity2)) + val result = attachmentDao.findByParentId("123") + + Assert.assertEquals(0, result!!.size) + } + + @Test + fun testFindBySubmissionId() = runTest { + val attachmentEntity = AttachmentEntity( + id = 1, contentType = "image/jpg", filename = "image.jpg", displayName = "File", url = "file.com", + createdAt = Date(), size = 10000, workerId = "123", submissionCommentId = 1, submissionId = 1 + ) + val attachmentEntity2 = attachmentEntity.copy(id = 2, workerId = "124", filename = "image2.jpg", submissionId = 2) + attachmentDao.insertAll(listOf(attachmentEntity, attachmentEntity2)) + + val result = attachmentDao.findBySubmissionId(1) + + Assert.assertEquals(listOf(attachmentEntity), result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testSubmissionCommentForeignKey() = runTest { + val attachmentEntity = AttachmentEntity( + id = 1, contentType = "image/jpg", filename = "image.jpg", displayName = "File", + url = "file.com", createdAt = Date(), size = 10000, workerId = "123", submissionCommentId = 2L + ) + + attachmentDao.insert(attachmentEntity) + } + + @Test + fun testSubmissionCommentCascade() = runTest { + val attachmentEntity = AttachmentEntity( + id = 1, contentType = "image/jpg", filename = "image.jpg", displayName = "File", url = "file.com", + createdAt = Date(), size = 10000, workerId = "123", submissionCommentId = 1, submissionId = 1L + ) + + attachmentDao.insert(attachmentEntity) + + submissionCommentDao.delete(SubmissionCommentEntity(SubmissionComment(1L), 1L, 1L)) + + val result = attachmentDao.findBySubmissionId(1L) + + Assert.assertTrue(result.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ConferenceDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ConferenceDaoTest.kt new file mode 100644 index 0000000000..a68a61803d --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ConferenceDaoTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.ConferenceEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ConferenceDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var conferenceDao: ConferenceDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + conferenceDao = db.conferenceDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1))) + courseDao.insert(CourseEntity(Course(2))) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByCourseId() = runTest { + val conferenceEntities = listOf( + ConferenceEntity(Conference(1), 1), + ConferenceEntity(Conference(2), 2) + ) + conferenceDao.insertAll(conferenceEntities) + + val result = conferenceDao.findByCourseId(1) + + Assert.assertEquals(conferenceEntities.filter { it.courseId == 1L }, result) + } + + @Test + fun testCourseCascade() = runTest { + val conferenceEntity = ConferenceEntity(Conference(1), 1) + + conferenceDao.insert(conferenceEntity) + + courseDao.delete(CourseEntity(Course(1))) + + val result = conferenceDao.findByCourseId(1) + + assert(result.isEmpty()) + } + + @Test(expected = SQLiteConstraintException::class) + fun testCourseForeignKey() = runTest { + val conferenceEntity = ConferenceEntity(Conference(1), 3) + + conferenceDao.insert(conferenceEntity) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + val conferenceEntity = ConferenceEntity(Conference(1), 1) + + conferenceDao.insert(conferenceEntity) + + val result = conferenceDao.findByCourseId(1) + + Assert.assertEquals(listOf(conferenceEntity), result) + + conferenceDao.deleteAllByCourseId(1L) + + val deletedResult = conferenceDao.findByCourseId(1L) + + assert(deletedResult.isEmpty()) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ConferenceRecordingDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ConferenceRecordingDaoTest.kt new file mode 100644 index 0000000000..26f001efde --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ConferenceRecordingDaoTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.ConferenceRecording +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.ConferenceEntity +import com.instructure.pandautils.room.offline.entities.ConferenceRecordingEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ConferenceRecordingDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var conferenceRecodingDao: ConferenceRecodingDao + private lateinit var conferenceDao: ConferenceDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + conferenceRecodingDao = db.conferenceRecordingDao() + conferenceDao = db.conferenceDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1))) + conferenceDao.insertAll( + listOf( + ConferenceEntity(Conference(1), 1), + ConferenceEntity(Conference(2), 1) + ) + ) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByCourseId() = runTest { + val conferenceRecordingEntity = ConferenceRecordingEntity(ConferenceRecording(recordingId = "recording1"), 1) + val conferenceRecordingEntity2 = ConferenceRecordingEntity(ConferenceRecording(recordingId = "recording2"), 2) + conferenceRecodingDao.insert(conferenceRecordingEntity) + conferenceRecodingDao.insert(conferenceRecordingEntity2) + + val result = conferenceRecodingDao.findByConferenceId(1) + + Assert.assertEquals(listOf(conferenceRecordingEntity), result) + } + + @Test + fun testConferenceCascade() = runTest { + val conferenceRecordingEntity = ConferenceRecordingEntity(ConferenceRecording(recordingId = "recording1"), 1) + + conferenceRecodingDao.insert(conferenceRecordingEntity) + + conferenceDao.delete(ConferenceEntity(Conference(1), 1)) + + val result = conferenceRecodingDao.findByConferenceId(1) + + assert(result.isEmpty()) + } + + @Test(expected = SQLiteConstraintException::class) + fun testConferenceForeignKey() = runTest { + val conferenceRecordingEntity = ConferenceRecordingEntity(ConferenceRecording(recordingId = "recording1"), 3) + + conferenceRecodingDao.insert(conferenceRecordingEntity) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseDaoTest.kt new file mode 100644 index 0000000000..6ca5ddf71d --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseDaoTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class CourseDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindAllEntities() = runTest { + val courseEntity = CourseEntity(Course(id = 1, "Course 1", "Original Course", "CRS", currentGrade = "0")) + val courseEntity2 = CourseEntity(Course(id = 2, "Course 2", "Original Course 2", "CRS", currentGrade = "2")) + courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) + + val result = courseDao.findAll() + + Assert.assertEquals(listOf(courseEntity, courseEntity2), result) + } + + @Test + fun testFindEntityById() = runTest { + val courseEntity = CourseEntity(Course(id = 1, "Course 1", "Original Course", "CRS", currentGrade = "0")) + val courseEntity2 = CourseEntity(Course(id = 2, "Course 2", "Original Course 2", "CRS", currentGrade = "2")) + courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) + + val result = courseDao.findById(1) + + Assert.assertEquals(courseEntity, result) + } + + @Test + fun testFindEntityByIdReturnsNullIfNotFound() = runTest { + val courseEntity = CourseEntity(Course(id = 1, "Course 1", "Original Course", "CRS", currentGrade = "0")) + val courseEntity2 = CourseEntity(Course(id = 2, "Course 2", "Original Course 2", "CRS", currentGrade = "2")) + courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) + + val result = courseDao.findById(3) + + Assert.assertNull(result) + } + + @Test + fun testFindEntitiesByIds() = runTest { + val courseEntity = CourseEntity(Course(id = 1, "Course 1", "Original Course", "CRS", currentGrade = "0")) + val courseEntity2 = CourseEntity(Course(id = 2, "Course 2", "Original Course 2", "CRS", currentGrade = "2")) + val courseEntity3 = CourseEntity(Course(id = 3, "Course 3", "Original Course 3", "CRS", currentGrade = "2")) + courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) + courseDao.insert(courseEntity3) + + val result = courseDao.findByIds(setOf(1, 2)) + + Assert.assertEquals(listOf(courseEntity, courseEntity2), result) + } + + @Test + fun testFindEntitiesByIdReturnsEmptyListNotFound() = runTest { + val courseEntity = CourseEntity(Course(id = 1, "Course 1", "Original Course", "CRS", currentGrade = "0")) + val courseEntity2 = CourseEntity(Course(id = 2, "Course 2", "Original Course 2", "CRS", currentGrade = "2")) + courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) + + val result = courseDao.findByIds(setOf(16, 55)) + + Assert.assertEquals(emptyList(), result) + } + + @Test + fun testDeleteByIds() = runTest { + val courseEntity = CourseEntity(Course(id = 1, "Course 1", "Original Course", "CRS", currentGrade = "0")) + val courseEntity2 = CourseEntity(Course(id = 2, "Course 2", "Original Course 2", "CRS", currentGrade = "2")) + courseDao.insertOrUpdate(courseEntity) + courseDao.insertOrUpdate(courseEntity2) + + val result = courseDao.findAll() + + Assert.assertEquals(listOf(courseEntity, courseEntity2), result) + + courseDao.deleteByIds(listOf(1, 2)) + + val deletedResult = courseDao.findAll() + + Assert.assertEquals(emptyList(), deletedResult) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseFeaturesDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseFeaturesDaoTest.kt new file mode 100644 index 0000000000..eb15d501ae --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseFeaturesDaoTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class CourseFeaturesDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var courseFeaturesDao: CourseFeaturesDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseFeaturesDao = db.courseFeaturesDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1))) + courseDao.insert(CourseEntity(Course(2))) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByCourseId() = runTest { + val courseFeaturesEntity = CourseFeaturesEntity(1, listOf("feature1", "feature2")) + val courseFeaturesEntity2 = CourseFeaturesEntity(2, listOf("feature3", "feature4")) + courseFeaturesDao.insert(courseFeaturesEntity) + courseFeaturesDao.insert(courseFeaturesEntity2) + + val result = courseFeaturesDao.findByCourseId(1) + + Assert.assertEquals(courseFeaturesEntity, result) + } + + @Test + fun testCourseCascade() = runTest { + val courseFeaturesEntity = CourseFeaturesEntity(1, listOf("feature1", "feature2")) + + courseFeaturesDao.insert(courseFeaturesEntity) + + courseDao.delete(CourseEntity(Course(1))) + + val result = courseFeaturesDao.findByCourseId(1) + + Assert.assertNull(result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testCourseForeignKey() = runTest { + val courseFeaturesEntity = CourseFeaturesEntity(3, listOf("feature1", "feature2")) + + courseFeaturesDao.insert(courseFeaturesEntity) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseGradingPeriodDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseGradingPeriodDaoTest.kt new file mode 100644 index 0000000000..0a729ee0de --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseGradingPeriodDaoTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseGradingPeriodEntity +import com.instructure.pandautils.room.offline.entities.GradingPeriodEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class CourseGradingPeriodDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var courseGradingPeriodDao: CourseGradingPeriodDao + private lateinit var gradingPeriodDao: GradingPeriodDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseGradingPeriodDao = db.courseGradingPeriodDao() + gradingPeriodDao = db.gradingPeriodDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(id = 1L))) + courseDao.insert(CourseEntity(Course(id = 2L))) + gradingPeriodDao.insert(GradingPeriodEntity(GradingPeriod(id = 1L))) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByCourseId() = runTest { + val courseGradingPeriodEntity = CourseGradingPeriodEntity(1L, 1L) + val courseGradingPeriodEntity2 = CourseGradingPeriodEntity(2L, 1L) + courseGradingPeriodDao.insert(courseGradingPeriodEntity) + courseGradingPeriodDao.insert(courseGradingPeriodEntity2) + + val result = courseGradingPeriodDao.findByCourseId(1) + + Assert.assertEquals(listOf(courseGradingPeriodEntity), result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testCourseForeignKey() = runTest { + val courseGradingPeriodEntity = CourseGradingPeriodEntity(3L, 1L) + + courseGradingPeriodDao.insert(courseGradingPeriodEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testGradingPeriodForeignKey() = runTest { + val courseGradingPeriodEntity = CourseGradingPeriodEntity(1L, 2L) + + courseGradingPeriodDao.insert(courseGradingPeriodEntity) + } + + @Test + fun testCourseCascade() = runTest { + val courseGradingPeriodEntity = CourseGradingPeriodEntity(1L, 1L) + + courseGradingPeriodDao.insert(courseGradingPeriodEntity) + + courseDao.delete(CourseEntity(Course(1L))) + + val result = courseGradingPeriodDao.findByCourseId(1L) + + assert(result.isEmpty()) + } + + @Test + fun testGradingPeriodCascade() = runTest { + val courseGradingPeriodEntity = CourseGradingPeriodEntity(1L, 1L) + + courseGradingPeriodDao.insert(courseGradingPeriodEntity) + + gradingPeriodDao.delete(GradingPeriodEntity(GradingPeriod(1L))) + + val result = courseGradingPeriodDao.findByCourseId(1L) + + assert(result.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSettingsDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSettingsDaoTest.kt new file mode 100644 index 0000000000..8e44c8e77f --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSettingsDaoTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteAbortException +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class CourseSettingsDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var courseSettingsDao: CourseSettingsDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseSettingsDao = db.courseSettingsDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val courseSettingsEntity = CourseSettingsEntity(1L, false, false) + val updated = courseSettingsEntity.copy(courseSummary = true) + + courseSettingsDao.insert(courseSettingsEntity) + courseSettingsDao.insert(updated) + + val result = courseSettingsDao.findByCourseId(1L) + + assertEquals(updated, result) + } + + @Test + fun testFindByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + val courseSettingsEntity = CourseSettingsEntity(1L, false, false) + val courseSettingsEntity2 = CourseSettingsEntity(2L, false, false) + + courseSettingsDao.insert(courseSettingsEntity) + courseSettingsDao.insert(courseSettingsEntity2) + + val result = courseSettingsDao.findByCourseId(2L) + + assertEquals(courseSettingsEntity2, result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testCourseForeignKey() = runTest { + val courseSettingsEntity = CourseSettingsEntity(1L, false, false) + + courseSettingsDao.insert(courseSettingsEntity) + } + + @Test + fun testDeleteCourseDeletesSettings() = runTest { + val courseEntity = CourseEntity(Course(1L)) + courseDao.insert(courseEntity) + + val courseSettingsEntity = CourseSettingsEntity(1L, false, false) + courseSettingsDao.insert(courseSettingsEntity) + + courseDao.delete(courseEntity) + + assertNull(courseSettingsDao.findByCourseId(1L)) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt new file mode 100644 index 0000000000..4f6432bacf --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDaoTest.kt @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.TabSyncData +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.UUID + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class CourseSyncProgressDaoTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private lateinit var db: OfflineDatabase + private lateinit var courseSyncProgressDao: CourseSyncProgressDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseSyncProgressDao = db.courseSyncProgressDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test(expected = SQLiteConstraintException::class) + fun testInsertError() = runTest { + val entity = CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + courseSyncProgressDao.insert(entity) + + val updatedEntity = entity.copy(progressState = ProgressState.COMPLETED) + courseSyncProgressDao.insert(updatedEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testInsertAllError() = runTest { + val entities = listOf( + CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ), + CourseSyncProgressEntity( + 2L, + UUID.randomUUID().toString(), + "Course 2", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + ) + + courseSyncProgressDao.insertAll(entities) + + val updatedEntity = entities.map { + it.copy(progressState = ProgressState.COMPLETED) + } + courseSyncProgressDao.insertAll(updatedEntity) + } + + + @Test + fun testFindAll() = runTest { + val entities = listOf( + CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ), + CourseSyncProgressEntity( + 2L, + UUID.randomUUID().toString(), + "Course 2", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + ) + + courseSyncProgressDao.insertAll(entities) + + val result = courseSyncProgressDao.findAll() + assertEquals(entities, result) + } + + @Test + fun testFindByCourseId() = runTest { + val entities = listOf( + CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ), + CourseSyncProgressEntity( + 2L, + UUID.randomUUID().toString(), + "Course 2", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + ) + + courseSyncProgressDao.insertAll(entities) + + val result = courseSyncProgressDao.findByCourseId(2L) + + assertEquals(entities[1], result) + } + + @Test + fun testDeleteAll() = runTest { + val entities = listOf( + CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ), + CourseSyncProgressEntity( + 2L, + UUID.randomUUID().toString(), + "Course 2", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + ) + + courseSyncProgressDao.insertAll(entities) + + courseSyncProgressDao.deleteAll() + + val result = courseSyncProgressDao.findAll() + assert(result.isEmpty()) + } + + @Test + fun testFindAllLiveData() = runTest { + val entities = listOf( + CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ), + CourseSyncProgressEntity( + 2L, + UUID.randomUUID().toString(), + "Course 2", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + ) + + courseSyncProgressDao.insertAll(entities) + + val result = courseSyncProgressDao.findAllLiveData() + result.observeForever { } + + assertEquals(entities, result.value) + } + + @Test + fun testFindByWorkerIdLiveData() = runTest { + val entities = listOf( + CourseSyncProgressEntity( + 1L, + UUID.randomUUID().toString(), + "Course 1", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ), + CourseSyncProgressEntity( + 2L, + UUID.randomUUID().toString(), + "Course 2", + CourseSyncSettingsEntity.TABS.associateWith { TabSyncData(it, ProgressState.IN_PROGRESS) }, + progressState = ProgressState.IN_PROGRESS + ) + ) + + courseSyncProgressDao.insertAll(entities) + + val result = courseSyncProgressDao.findByWorkerIdLiveData(entities[1].workerId) + result.observeForever { } + + assertEquals(entities[1], result.value) + } + +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncSettingsDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncSettingsDaoTest.kt new file mode 100644 index 0000000000..38fa972449 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/CourseSyncSettingsDaoTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class CourseSyncSettingsSD { + + private lateinit var db: OfflineDatabase + private lateinit var courseSyncSettingsDao: CourseSyncSettingsDao + private lateinit var fileSyncSettingsDao: FileSyncSettingsDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseSyncSettingsDao = db.courseSyncSettingsDao() + fileSyncSettingsDao = db.fileSyncSettingsDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindAllEntities() = runTest { + val courseSyncSettingsEntity = CourseSyncSettingsEntity( + courseId = 1L, + courseName = "Course 1", + fullContentSync = false + ) + val courseSyncSettingsEntity2 = CourseSyncSettingsEntity( + courseId = 2L, + courseName = "Course 2", + fullContentSync = false + ) + courseSyncSettingsDao.insert(courseSyncSettingsEntity) + courseSyncSettingsDao.insert(courseSyncSettingsEntity2) + + val result = courseSyncSettingsDao.findAll() + + assertEquals(listOf(courseSyncSettingsEntity, courseSyncSettingsEntity2), result) + } + + @Test + fun testInsertReplace() = runTest { + val courseSyncSettingsEntity = CourseSyncSettingsEntity( + courseId = 1L, + courseName = "Course 1", + fullContentSync = false + ) + + courseSyncSettingsDao.insert(courseSyncSettingsEntity) + + val updated = courseSyncSettingsEntity.copy(fullContentSync = true) + courseSyncSettingsDao.insert(updated) + + val result = courseSyncSettingsDao.findById(1L) + + assertEquals(updated, result) + } + + @Test + fun testFindById() = runTest { + val courseSyncSettingsEntity = CourseSyncSettingsEntity( + courseId = 1L, + courseName = "Course 1", + fullContentSync = false + ) + val courseSyncSettingsEntity2 = CourseSyncSettingsEntity( + courseId = 2L, + courseName = "Course 2", + fullContentSync = false + ) + courseSyncSettingsDao.insert(courseSyncSettingsEntity) + courseSyncSettingsDao.insert(courseSyncSettingsEntity2) + + val result = courseSyncSettingsDao.findById(2L) + + assertEquals(courseSyncSettingsEntity2, result) + } + + @Test + fun testFindByIdList() = runTest { + val courseSyncSettingsEntity = CourseSyncSettingsEntity( + courseId = 1L, + courseName = "Course 1", + fullContentSync = false + ) + val courseSyncSettingsEntity2 = CourseSyncSettingsEntity( + courseId = 2L, + courseName = "Course 2", + fullContentSync = false + ) + + val courseSyncSettingsEntity3 = CourseSyncSettingsEntity( + courseId = 3L, + courseName = "Course 3", + fullContentSync = false + ) + courseSyncSettingsDao.insert(courseSyncSettingsEntity) + courseSyncSettingsDao.insert(courseSyncSettingsEntity2) + courseSyncSettingsDao.insert(courseSyncSettingsEntity3) + + val result = courseSyncSettingsDao.findByIds(listOf(1L, 3L)) + + assertEquals(listOf(courseSyncSettingsEntity, courseSyncSettingsEntity3), result) + } + + @Test + fun findWithFiles() = runTest { + val courseSyncSettingsEntity = CourseSyncSettingsEntity( + courseId = 1L, + courseName = "Course 1", + fullContentSync = false + ) + val courseSyncSettingsEntity2 = CourseSyncSettingsEntity( + courseId = 2L, + courseName = "Course 2", + fullContentSync = false + ) + courseSyncSettingsDao.insert(courseSyncSettingsEntity) + courseSyncSettingsDao.insert(courseSyncSettingsEntity2) + + val fileSyncSettingsEntity = FileSyncSettingsEntity(1L, "", 1L, null) + val fileSyncSettingsEntity2 = FileSyncSettingsEntity(2L, "", 1L, null) + val fileSyncSettingsEntity3 = FileSyncSettingsEntity(3L, "", 2L, null) + val fileSyncSettingsEntity4 = FileSyncSettingsEntity(4L, "", 2L, null) + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + fileSyncSettingsDao.insert(fileSyncSettingsEntity2) + fileSyncSettingsDao.insert(fileSyncSettingsEntity3) + fileSyncSettingsDao.insert(fileSyncSettingsEntity4) + + val result1 = courseSyncSettingsDao.findWithFilesById(1L) + assertEquals( + CourseSyncSettingsWithFiles( + courseSyncSettingsEntity, + listOf(fileSyncSettingsEntity, fileSyncSettingsEntity2) + ), result1 + ) + + val result2 = courseSyncSettingsDao.findWithFilesById(2L) + assertEquals( + CourseSyncSettingsWithFiles( + courseSyncSettingsEntity2, + listOf(fileSyncSettingsEntity3, fileSyncSettingsEntity4) + ), result2 + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DashboardCardDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DashboardCardDaoTest.kt new file mode 100644 index 0000000000..fcb43e1b1d --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DashboardCardDaoTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.DashboardCard +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.DashboardCardEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DashboardCardDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var dashboardCardDao: DashboardCardDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + dashboardCardDao = db.dashboardCardDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindAllEntities() = runTest { + val entities = listOf( + DashboardCardEntity(DashboardCard(id = 1, shortName = "No more tests please")), + DashboardCardEntity(DashboardCard(id = 2, shortName = "No more tests please 2")), + ) + dashboardCardDao.insertAll(entities) + + val result = dashboardCardDao.findAll() + + Assert.assertEquals(entities, result) + } + + @Test + fun testUpdatingEntities() = runTest { + val entities = listOf( + DashboardCardEntity(DashboardCard(id = 1, shortName = "No more tests please")), + DashboardCardEntity(DashboardCard(id = 2, shortName = "No more tests please 2")), + ) + + val newEntities = listOf( + DashboardCardEntity(DashboardCard(id = 1, shortName = "No more tests please")), + DashboardCardEntity(DashboardCard(id = 3, shortName = "No more tests please 3")), + ) + dashboardCardDao.insertAll(entities) + + dashboardCardDao.updateEntities(newEntities) + val result = dashboardCardDao.findAll() + + Assert.assertEquals(newEntities, result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionEntryDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionEntryDaoTest.kt new file mode 100644 index 0000000000..6ea02fa500 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionEntryDaoTest.kt @@ -0,0 +1,75 @@ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.DiscussionEntryEntity +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DiscussionEntryDaoTest { + private lateinit var db: OfflineDatabase + + private lateinit var discussionEntryDao: DiscussionEntryDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + discussionEntryDao = db.discussionEntryDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + val discussionEntryEntity = DiscussionEntryEntity(DiscussionEntry(id = 1L, message = "Discussion 1")) + discussionEntryDao.insert(discussionEntryEntity) + + val updated = discussionEntryEntity.copy(message = "updated") + discussionEntryDao.insert(updated) + + val result = discussionEntryDao.findById(1L) + + Assert.assertEquals(updated.message, result?.message) + } + + @Test + fun testInsertAllReplace() = runTest { + val discussionEntryEntities = listOf(DiscussionEntryEntity(DiscussionEntry(id = 1L, message = "Discussion 1")), DiscussionEntryEntity(DiscussionEntry(id = 2L, message = "Discussion 2"))) + discussionEntryDao.insertAll(discussionEntryEntities) + + val updated = discussionEntryEntities.map { it.copy(message = "updated") } + discussionEntryDao.insertAll(updated) + + val result0 = discussionEntryDao.findById(1L) + val result1 = discussionEntryDao.findById(2L) + + Assert.assertEquals(updated[0].message, result0?.message) + Assert.assertEquals(updated[1].message, result1?.message) + } + + fun testFindById() = runTest { + val discussionEntryEntities = listOf(DiscussionEntryEntity(DiscussionEntry(id = 1L, message = "Discussion 1")), DiscussionEntryEntity(DiscussionEntry(id = 2L, message = "Discussion 2"))) + discussionEntryDao.insertAll(discussionEntryEntities) + + val result = discussionEntryDao.findById(1L) + + Assert.assertEquals(discussionEntryEntities[0], result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionParticipantDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionParticipantDaoTest.kt new file mode 100644 index 0000000000..684a9fe280 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionParticipantDaoTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DiscussionParticipantDaoTest { + + private lateinit var db: OfflineDatabase + + private lateinit var discussionParticipantDao: DiscussionParticipantDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + discussionParticipantDao = db.discussionParticipantDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + val discussionParticipantEntity = DiscussionParticipantEntity(DiscussionParticipant(id = 1L, displayName = "Participant")) + discussionParticipantDao.insert(discussionParticipantEntity) + + val updated = discussionParticipantEntity.copy(displayName = "updated") + discussionParticipantDao.insert(updated) + + val result = discussionParticipantDao.findById(1L) + + Assert.assertEquals(updated.displayName, result?.displayName) + } + + @Test + fun testFindById() = runTest { + val discussionParticipantEntity = DiscussionParticipantEntity(DiscussionParticipant(id = 1L, displayName = "Participant")) + val discussionParticipantEntity2 = DiscussionParticipantEntity(DiscussionParticipant(id = 2L, displayName = "Participant 2")) + discussionParticipantDao.insertAll(listOf(discussionParticipantEntity, discussionParticipantEntity2)) + + val result = discussionParticipantDao.findById(1L) + + Assert.assertEquals(discussionParticipantEntity.displayName, result?.displayName) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicDaoTest.kt new file mode 100644 index 0000000000..4f74102695 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicDaoTest.kt @@ -0,0 +1,79 @@ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.DiscussionTopicEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DiscussionTopicDaoTest { + private lateinit var db: OfflineDatabase + + private lateinit var discussionTopicDao: DiscussionTopicDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + discussionTopicDao = db.discussionTopicDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + val discussionTopicEntity = DiscussionTopicEntity(DiscussionTopic(mutableListOf(1L, 2L)), emptyList(), emptyList(), 1L) + discussionTopicDao.insert(discussionTopicEntity) + + val updated = discussionTopicEntity.copy(unreadEntries = mutableListOf(3L, 4L)) + discussionTopicDao.insert(updated) + + val result = discussionTopicDao.findById(1L) + + Assert.assertEquals(updated.unreadEntries, result?.unreadEntries) + } + + @Test + fun testInsertAllReplace() = runTest { + val discussionTopicEntities = listOf( + DiscussionTopicEntity(DiscussionTopic(mutableListOf(1L, 2L)), emptyList(), emptyList(), 1L), + DiscussionTopicEntity(DiscussionTopic(mutableListOf(1L, 2L)), emptyList(), emptyList(), 2L) + ) + discussionTopicDao.insertAll(discussionTopicEntities) + + val updated = discussionTopicEntities.map { it.copy(unreadEntries = mutableListOf(3L, 4L)) } + discussionTopicDao.insertAll(updated) + + val result0 = discussionTopicDao.findById(1L) + val result1 = discussionTopicDao.findById(2L) + + Assert.assertEquals(updated[0].unreadEntries, result0?.unreadEntries) + Assert.assertEquals(updated[1].unreadEntries, result1?.unreadEntries) + } + + fun testFindById() = runTest { + val discussionTopicEntities = listOf( + DiscussionTopicEntity(DiscussionTopic(mutableListOf(1L, 2L)), emptyList(), emptyList(), 1L), + DiscussionTopicEntity(DiscussionTopic(mutableListOf(1L, 2L)), emptyList(), emptyList(), 2L) + ) + discussionTopicDao.insertAll(discussionTopicEntities) + + val result = discussionTopicDao.findById(1L) + + Assert.assertEquals(discussionTopicEntities[0], result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicHeaderDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicHeaderDaoTest.kt new file mode 100644 index 0000000000..6104b5cd73 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicHeaderDaoTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicHeaderEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.time.OffsetDateTime +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DiscussionTopicHeaderDaoTest { + + private lateinit var db: OfflineDatabase + + private lateinit var discussionTopicHeaderDao: DiscussionTopicHeaderDao + private lateinit var courseDao: CourseDao + private lateinit var discussionParticipantDao: DiscussionParticipantDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + discussionTopicHeaderDao = db.discussionTopicHeaderDao() + courseDao = db.courseDao() + discussionParticipantDao = db.discussionParticipantDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + + val discussionTopicHeaderEntity = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + discussionTopicHeaderDao.insert(discussionTopicHeaderEntity) + + val updated = discussionTopicHeaderEntity.copy(title = "updated") + discussionTopicHeaderDao.insert(updated) + + val result = discussionTopicHeaderDao.findById(1L) + + Assert.assertEquals(updated.title, result?.title) + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + + val discussionTopicHeaderEntity = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + val discussionTopicHeaderEntity2 = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 2L, title = "Discussion 2"), 1) + discussionTopicHeaderDao.insertAll(listOf(discussionTopicHeaderEntity, discussionTopicHeaderEntity2)) + + val result = discussionTopicHeaderDao.findById(1L) + + Assert.assertEquals(discussionTopicHeaderEntity.title, result?.title) + } + + @Test + fun findAllDiscussionsOnlyReturnsDiscussionsForSpecificCourse() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + courseDao.insert(CourseEntity(Course(id = 2))) + + val discussion = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + val discussion2 = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 2L, title = "Discussion 2"), 1) + val discussion3 = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 3L, title = "Discussion 3"), 2) + val announcement = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 4L, title = "Announcement", announcement = true), 1) + discussionTopicHeaderDao.insertAll(listOf(discussion, discussion2, discussion3, announcement)) + + val result = discussionTopicHeaderDao.findAllDiscussionsForCourse(1) + + Assert.assertEquals(2, result.size) + Assert.assertEquals(discussion.title, result[0].title) + Assert.assertEquals(discussion2.title, result[1].title) + } + + @Test + fun findAllAnnouncementsOnlyReturnsAnnouncementsForSpecificCourseAndOrdersByDate() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + courseDao.insert(CourseEntity(Course(id = 2))) + + val discussion = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + val announcement = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 2L, title = "Announcement", announcement = true), 2) + val date1 = Date(OffsetDateTime.now().toEpochSecond()) + val date2 = Date(OffsetDateTime.now().plusDays(2).toEpochSecond()) + val announcement2 = + DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 3L, title = "Announcement 2", announcement = true, postedDate = date1), 1) + val announcement3 = + DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 4L, title = "Announcement 3", announcement = true, postedDate = date2), 1) + discussionTopicHeaderDao.insertAll(listOf(discussion, announcement, announcement2, announcement3)) + + val result = discussionTopicHeaderDao.findAllAnnouncementsForCourse(1) + + Assert.assertEquals(2, result.size) + Assert.assertEquals(announcement3.title, result[0].title) + Assert.assertEquals(announcement2.title, result[1].title) + } + + @Test(expected = SQLiteConstraintException::class) + fun testCourseForeignKeyIsMandatory() = runTest { + val discussion = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + + discussionTopicHeaderDao.insert(discussion) + } + + @Test + fun testDeletingTheAssociatedCourseDeletesTheDiscussion() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + val discussion = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + discussionTopicHeaderDao.insert(discussion) + + courseDao.delete(CourseEntity(Course(id = 1))) + + val result = discussionTopicHeaderDao.findAllDiscussionsForCourse(1) + + Assert.assertTrue(result.isEmpty()) + } + + @Test + fun testDeletingTheAssociatedParticipantSetsItToNull() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + val discussion = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion", author = DiscussionParticipant(id = 1)), 1) + val participant = DiscussionParticipantEntity(DiscussionParticipant(id = 1, displayName = "Participant")) + + discussionParticipantDao.insert(participant) + discussionTopicHeaderDao.insert(discussion) + + val resultBeforeDelete = discussionTopicHeaderDao.findById(1) + Assert.assertEquals(1L, resultBeforeDelete?.authorId) + + discussionParticipantDao.delete(participant) + + val result = discussionTopicHeaderDao.findById(1) + Assert.assertNull(result?.authorId) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + + val discussionTopicHeaderEntity = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 1L, title = "Discussion"), 1) + val discussionTopicHeaderEntity2 = DiscussionTopicHeaderEntity(DiscussionTopicHeader(id = 2L, title = "Discussion 2"), 1) + discussionTopicHeaderDao.insertAll(listOf(discussionTopicHeaderEntity, discussionTopicHeaderEntity2)) + + val result = discussionTopicHeaderDao.findAllDiscussionsForCourse(1L) + + Assert.assertEquals(listOf(discussionTopicHeaderEntity, discussionTopicHeaderEntity2), result) + + discussionTopicHeaderDao.deleteAllByCourseId(1L, false) + + val deletedResult = discussionTopicHeaderDao.findAllDiscussionsForCourse(1L) + + Assert.assertTrue(deletedResult.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicPermissionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicPermissionDaoTest.kt new file mode 100644 index 0000000000..5aec99c3e8 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicPermissionDaoTest.kt @@ -0,0 +1,99 @@ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.DiscussionTopicPermission +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicHeaderEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicPermissionEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DiscussionTopicPermissionDaoTest { + private lateinit var db: OfflineDatabase + + private lateinit var discussionTopicPermissionDao: DiscussionTopicPermissionDao + private lateinit var discussionTopicHeaderDao: DiscussionTopicHeaderDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + discussionTopicPermissionDao = db.discussionTopicPermissionDao() + discussionTopicHeaderDao = db.discussionTopicHeaderDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + discussionTopicHeaderDao.insert(DiscussionTopicHeaderEntity(DiscussionTopicHeader(1L), 1L)) + + val discussionTopicPermissionEntity = DiscussionTopicPermissionEntity(DiscussionTopicPermission(true), 1L) + val id = discussionTopicPermissionDao.insert(discussionTopicPermissionEntity) + + val updated = discussionTopicPermissionEntity.copy(id = id, attach = false) + discussionTopicPermissionDao.insert(updated) + + val result = discussionTopicPermissionDao.findByDiscussionTopicHeaderId(1L) + + Assert.assertEquals(updated.attach, result?.attach) + } + + @Test + fun testInsertAllReplace() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + discussionTopicHeaderDao.insert(DiscussionTopicHeaderEntity(DiscussionTopicHeader(1L), 1L)) + discussionTopicHeaderDao.insert(DiscussionTopicHeaderEntity(DiscussionTopicHeader(2L), 1L)) + + val discussionEntryEntities = listOf( + DiscussionTopicPermissionEntity(DiscussionTopicPermission(true, true), 1L), + DiscussionTopicPermissionEntity(DiscussionTopicPermission(true), 2L) + ) + val ids = discussionTopicPermissionDao.insertAll(discussionEntryEntities) + + val updated = discussionEntryEntities.mapIndexed { index, entity -> entity.copy(id = ids[index], attach = false) } + discussionTopicPermissionDao.insertAll(updated) + + val result0 = discussionTopicPermissionDao.findByDiscussionTopicHeaderId(1L) + val result1 = discussionTopicPermissionDao.findByDiscussionTopicHeaderId(2L) + + Assert.assertEquals(updated[0].attach, result0?.attach) + Assert.assertEquals(updated[1].attach, result1?.attach) + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + discussionTopicHeaderDao.insert(DiscussionTopicHeaderEntity(DiscussionTopicHeader(1L), 1L)) + discussionTopicHeaderDao.insert(DiscussionTopicHeaderEntity(DiscussionTopicHeader(2L), 1L)) + + val discussionEntryEntities = listOf( + DiscussionTopicPermissionEntity(DiscussionTopicPermission(true, true), 1L), + DiscussionTopicPermissionEntity(DiscussionTopicPermission(true), 2L) + ) + val ids = discussionTopicPermissionDao.insertAll(discussionEntryEntities) + + val result = discussionTopicPermissionDao.findByDiscussionTopicHeaderId(1L) + + Assert.assertEquals(discussionEntryEntities[0].copy(id = ids[0]), result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/EditDashboardItemDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/EditDashboardItemDaoTest.kt new file mode 100644 index 0000000000..269ed64748 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/EditDashboardItemDaoTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity +import com.instructure.pandautils.room.offline.entities.EnrollmentState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class EditDashboardItemDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var editDashboardItemDao: EditDashboardItemDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + editDashboardItemDao = db.editDashboardItemDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByEnrollmentStateAndOrderByPosition() = runTest { + val item1 = EditDashboardItemEntity(courseId = 1, name = "Course 1", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 2) + val item2 = EditDashboardItemEntity(courseId = 2, name = "Course 2", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 1) + val item3 = EditDashboardItemEntity(courseId = 3, name = "Course 3", isFavorite = true, enrollmentState = EnrollmentState.PAST, position = 3) + + editDashboardItemDao.insertAll(listOf(item1, item2, item3)) + + val result = editDashboardItemDao.findByEnrollmentState(EnrollmentState.CURRENT) + assertEquals(listOf(item2, item1), result) + } + + @Test + fun testUpdateItemsDropsAllPreviousItems() = runTest { + val item1 = EditDashboardItemEntity(courseId = 1, name = "Course 1", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 2) + val item2 = EditDashboardItemEntity(courseId = 2, name = "Course 2", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 1) + val item3 = EditDashboardItemEntity(courseId = 3, name = "Course 3", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 3) + + editDashboardItemDao.insertAll(listOf(item1, item2)) + + val result = editDashboardItemDao.findByEnrollmentState(EnrollmentState.CURRENT) + assertEquals(listOf(item2, item1), result) + + editDashboardItemDao.updateEntities(listOf(item3, item1)) + + val updatedResult = editDashboardItemDao.findByEnrollmentState(EnrollmentState.CURRENT) + assertEquals(listOf(item1, item3), updatedResult) + } + + @Test + fun testInsertReplace() = runTest { + val item = EditDashboardItemEntity(courseId = 1, name = "Course 1", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 2) + val updated = EditDashboardItemEntity(courseId = 1, name = "Course Updated", isFavorite = true, enrollmentState = EnrollmentState.CURRENT, position = 2) + + editDashboardItemDao.insertAll(listOf(item)) + editDashboardItemDao.insertAll(listOf(updated)) + + val result = editDashboardItemDao.findByEnrollmentState(EnrollmentState.CURRENT) + + assertEquals(updated, result[0]) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/EnrollmentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/EnrollmentDaoTest.kt new file mode 100644 index 0000000000..8771eab4a0 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/EnrollmentDaoTest.kt @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class EnrollmentDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var enrollmentDao: EnrollmentDao + private lateinit var userDao: UserDao + private lateinit var sectionDao: SectionDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + enrollmentDao = db.enrollmentDao() + userDao = db.userDao() + sectionDao = db.sectionDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(id = 1))) + courseDao.insert(CourseEntity(Course(id = 2))) + userDao.insert(UserEntity(User(id = 1))) + userDao.insert(UserEntity(User(id = 2))) + userDao.insert(UserEntity(User(id = 3))) + sectionDao.insert(SectionEntity(Section(id = 1), 1)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindAllEntities() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 1), 1, 1, 1) + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val result = enrollmentDao.findAll() + + Assert.assertEquals(entities, result) + } + + @Test + fun testFindByCourseId() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 1), 2, 1, 1) + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val result = enrollmentDao.findByCourseId(1) + + Assert.assertEquals(1, result.size) + Assert.assertEquals(entities.first(), result.first()) + } + + @Test + fun testFindByGradingPeriodId() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1, currentGradingPeriodId = 1), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 1), 2, 1, 1) + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val result = enrollmentDao.findByGradingPeriodId(1) + + Assert.assertEquals(1, result.size) + Assert.assertEquals(entities.first(), result.first()) + } + + @Test(expected = SQLiteConstraintException::class) + fun testUserForeignKey() = runTest { + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 4) + + enrollmentDao.insert(enrollmentEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testObservedUserForeignKey() = runTest { + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 4) + + enrollmentDao.insert(enrollmentEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testSectionForeignKey() = runTest { + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 2, 1) + + enrollmentDao.insert(enrollmentEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testCourseForeignKey() = runTest { + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 3, 1, 1) + + enrollmentDao.insert(enrollmentEntity) + } + + @Test + fun testObservedUserSetNullOnDelete() = runTest { + userDao.insert(UserEntity(User(id = 2))) + + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 2) + + enrollmentDao.insert(enrollmentEntity) + + userDao.delete(UserEntity(User(id = 2))) + + val result = enrollmentDao.findAll() + + Assert.assertEquals(listOf(enrollmentEntity.copy(observedUserId = null)), result) + } + + @Test + fun testSectionSetNullOnDelete() = runTest { + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 1) + + enrollmentDao.insert(enrollmentEntity) + + sectionDao.delete(SectionEntity(Section(1))) + + val result = enrollmentDao.findAll() + + Assert.assertEquals(listOf(enrollmentEntity.copy(courseSectionId = null)), result) + } + + @Test + fun testCourseCascade() = runTest { + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 1), 1, 1, 1) + + enrollmentDao.insert(enrollmentEntity) + + courseDao.delete(CourseEntity(Course(1))) + + val result = enrollmentDao.findAll() + + assert(result.isEmpty()) + } + + @Test + fun testUserCascade() = runTest { + userDao.insert(UserEntity(User(id = 4))) + + val enrollmentEntity = EnrollmentEntity(Enrollment(id = 1, userId = 4), 1, 1, 1) + + enrollmentDao.insert(enrollmentEntity) + + userDao.delete(UserEntity(User(id = 4))) + + val result = enrollmentDao.findAll() + + assert(result.isEmpty()) + } + + @Test + fun testFindByCourseIdAndRoleTeachers() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1, role = Enrollment.EnrollmentType.Student), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 2, role = Enrollment.EnrollmentType.Teacher), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 3, userId = 3, role = Enrollment.EnrollmentType.Teacher), 1, 1, 1), + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val result = enrollmentDao.findByCourseIdAndRole(1, Enrollment.EnrollmentType.Teacher.name) + + Assert.assertEquals(entities.filter { it.role == Enrollment.EnrollmentType.Teacher.name }, result) + } + + @Test + fun testFindByCourseIdAndRoleStudents() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1, role = Enrollment.EnrollmentType.Student), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 2, role = Enrollment.EnrollmentType.Teacher), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 3, userId = 3, role = Enrollment.EnrollmentType.Student), 1, 1, 1), + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val result = enrollmentDao.findByCourseIdAndRole(1, Enrollment.EnrollmentType.Student.name) + + Assert.assertEquals(entities.filter { it.role == Enrollment.EnrollmentType.Student.name }, result) + } + + @Test + fun testFindByCourseIdAndRoleCourses() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1, role = Enrollment.EnrollmentType.Student), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 2, role = Enrollment.EnrollmentType.Teacher), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 3, userId = 3, role = Enrollment.EnrollmentType.Teacher), 2, 1, 1), + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val result = enrollmentDao.findByCourseIdAndRole(1, Enrollment.EnrollmentType.Teacher.name) + + Assert.assertEquals(entities.filter { it.role == Enrollment.EnrollmentType.Teacher.name && it.courseId == 1L }, result) + } + + @Test + fun testFindByUserId() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1, role = Enrollment.EnrollmentType.Student), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 2, role = Enrollment.EnrollmentType.Teacher), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 3, userId = 3, role = Enrollment.EnrollmentType.Teacher), 2, 1, 1), + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val expected = entities.first { it.userId == 1L } + + val result = enrollmentDao.findByUserId(1L) + + Assert.assertEquals(expected, result) + } + + @Test + fun testFindUSerByNonExistingUserId() = runTest { + val entities = listOf( + EnrollmentEntity(Enrollment(id = 1, userId = 1, role = Enrollment.EnrollmentType.Student), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 2, userId = 2, role = Enrollment.EnrollmentType.Teacher), 1, 1, 1), + EnrollmentEntity(Enrollment(id = 3, userId = 3, role = Enrollment.EnrollmentType.Teacher), 2, 1, 1), + ) + entities.forEach { + enrollmentDao.insert(it) + } + + val expected = null + + val result = enrollmentDao.findByUserId(4L) + + Assert.assertEquals(expected, result) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt new file mode 100644 index 0000000000..be60845c7a --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileFolderDaoTest.kt @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.util.Date + +@ExperimentalCoroutinesApi +class FileFolderDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var fileFolderDao: FileFolderDao + private lateinit var localFileDao: LocalFileDao + private lateinit var fileSyncSettingsDao: FileSyncSettingsDao + private lateinit var courseDao: CourseDao + private lateinit var courseSyncSettingsDao: CourseSyncSettingsDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + fileFolderDao = db.fileFolderDao() + localFileDao = db.localFileDao() + fileSyncSettingsDao = db.fileSyncSettingsDao() + courseDao = db.courseDao() + courseSyncSettingsDao = db.courseSyncSettingsDao() + } + + @Test + fun testInsertReplace() = runTest { + val fileFolder = FileFolderEntity(FileFolder(id = 1L, name = "original")) + val updated = FileFolderEntity(FileFolder(id = 1L, name = "updated")) + + fileFolderDao.insert(fileFolder) + fileFolderDao.insert(updated) + + val result = fileFolderDao.findById(1L) + + assertEquals(updated, result) + } + + @Test + fun testDeleteAll() = runTest { + val files = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "original1")), + FileFolderEntity(FileFolder(id = 2L, name = "original2")) + ) + + fileFolderDao.insertAll(files) + fileFolderDao.deleteAllByCourseId(1L) + + val result = fileFolderDao.findAllFilesByCourseId(1L) + + assertEquals(emptyList(), result) + } + + @Test + fun testFindAllByCourseId() = runTest { + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + contextId = 1L, + contextType = "Course", + name = "folder", + parentFolderId = 0 + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + contextId = 1L, + contextType = "Course", + name = "folder2", + parentFolderId = 0 + ) + ), + FileFolderEntity( + FileFolder( + id = 3L, + contextId = 2L, + contextType = "Course", + name = "folder2", + parentFolderId = 0 + ) + ) + ) + val files = listOf( + FileFolderEntity(FileFolder(id = 4L, name = "file1", folderId = 1L)), + FileFolderEntity(FileFolder(id = 5L, name = "file2", folderId = 2L)), + FileFolderEntity(FileFolder(id = 6L, name = "file3", folderId = 3L)) + ) + + fileFolderDao.insertAll(folders) + fileFolderDao.insertAll(files) + + val result = fileFolderDao.findAllFilesByCourseId(1L) + + assertEquals(files.subList(0, 2), result) + } + + @Test + fun testFindById() = runTest { + val files = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "file1")), + FileFolderEntity(FileFolder(id = 2L, name = "file2")) + ) + + fileFolderDao.insertAll(files) + + val result = fileFolderDao.findById(1L) + + assertEquals(files[0], result) + } + + @Test + fun testVisibleFindFoldersByParentId() = runTest { + val folders = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "folder1", parentFolderId = 0)), + FileFolderEntity(FileFolder(id = 2L, name = "folder2", parentFolderId = 0)), + FileFolderEntity(FileFolder(id = 3L, name = "folder3", parentFolderId = 1L)), + FileFolderEntity(FileFolder(id = 4L, name = "folder4", parentFolderId = 1L, isHidden = true)), + FileFolderEntity(FileFolder(id = 7L, name = "folder7", parentFolderId = 1L)), + FileFolderEntity(FileFolder(id = 8L, name = "folder8", parentFolderId = 1L, isHiddenForUser = true)), + FileFolderEntity(FileFolder(id = 5L, name = "folder5", parentFolderId = 2L)), + FileFolderEntity(FileFolder(id = 6L, name = "folder6", parentFolderId = 2L)) + ) + + fileFolderDao.insertAll(folders) + + val result = fileFolderDao.findVisibleFoldersByParentId(1L) + + assertEquals(2, result.size) + assertEquals("folder3", result.first().name) + assertEquals("folder7", result.last().name) + } + + @Test + fun testFindVisibleFilesByFolderId() = runTest { + val folders = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "folder1", parentFolderId = 0)), + FileFolderEntity(FileFolder(id = 2L, name = "folder2", parentFolderId = 0)) + ) + + val files = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "file1", folderId = 1L, isHidden = true)), + FileFolderEntity(FileFolder(id = 7L, name = "file5", folderId = 1L, isHiddenForUser = true)), + FileFolderEntity(FileFolder(id = 8L, name = "file6", folderId = 1L)), + FileFolderEntity(FileFolder(id = 4L, name = "file2", folderId = 1L)), + FileFolderEntity(FileFolder(id = 5L, name = "file3", folderId = 2L)), + FileFolderEntity(FileFolder(id = 6L, name = "file4", folderId = 2L)) + ) + + fileFolderDao.insertAll(folders) + fileFolderDao.insertAll(files) + + val result = fileFolderDao.findVisibleFilesByFolderId(1L) + + assertEquals(2, result.size) + assertEquals("file2", result.first().name) + assertEquals("file6", result.last().name) + } + + @Test + fun testFindRootFolderForContext() = runTest { + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + name = "folder2", + parentFolderId = 0, + contextId = 2L, + contextType = "Course" + ) + ), + ) + + fileFolderDao.insertAll(folders) + + val result = fileFolderDao.findRootFolderForContext(1L) + + assertEquals(folders[0], result) + } + + @Test + fun testReplaceAll() = runTest { + val files = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "file1", folderId = 1L, contextId = 1L)), + FileFolderEntity(FileFolder(id = 2L, name = "file2", folderId = 1L, contextId = 1L)) + ) + + val newFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "file3", folderId = 1L, contextId = 1L)), + FileFolderEntity(FileFolder(id = 4L, name = "file4", folderId = 1L, contextId = 1L)) + ) + + fileFolderDao.insertAll(files) + + fileFolderDao.replaceAll(newFiles, 1L) + + val result = fileFolderDao.findVisibleFilesByFolderId(1L) + + assertEquals(newFiles, result) + } + + @Test + fun testFindFilesToSyncCreatedDate() = runTest { + courseDao.insert(CourseEntity(Course(id = 1L, name = "course1"))) + courseDao.insert(CourseEntity(Course(id = 2L, name = "course2"))) + + val localFiles = listOf( + LocalFileEntity(id = 3L, path = "file1", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 4L, path = "file2", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 5L, path = "file3", courseId = 2L, createdDate = Date(1000)), + LocalFileEntity(id = 6L, path = "file4", courseId = 2L, createdDate = Date(1000)) + ) + + localFiles.forEach { localFileDao.insert(it) } + + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + name = "folder2", + parentFolderId = 0, + contextId = 2L, + contextType = "Course" + ) + ), + ) + + fileFolderDao.insertAll(folders) + + val remoteFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "file1", folderId = 1L, createdDate = Date(2))), + FileFolderEntity(FileFolder(id = 4L, name = "file2", folderId = 1L, createdDate = Date(1))), + FileFolderEntity(FileFolder(id = 5L, name = "file3", folderId = 2L, createdDate = Date(1000))), + FileFolderEntity(FileFolder(id = 6L, name = "file4", folderId = 2L, createdDate = Date(1000))) + ) + + fileFolderDao.insertAll(remoteFiles) + + val result = fileFolderDao.findFilesToSync(1L, true) + + assertEquals(listOf(remoteFiles[0]), result) + } + + @Test + fun testFindFilesToSyncUpdatedDate() = runTest { + courseDao.insert(CourseEntity(Course(id = 1L, name = "course1"))) + courseDao.insert(CourseEntity(Course(id = 2L, name = "course2"))) + + val localFiles = listOf( + LocalFileEntity(id = 3L, path = "file1", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 4L, path = "file2", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 5L, path = "file3", courseId = 2L, createdDate = Date(1000)), + LocalFileEntity(id = 6L, path = "file4", courseId = 2L, createdDate = Date(1000)) + ) + + localFiles.forEach { localFileDao.insert(it) } + + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + name = "folder2", + parentFolderId = 0, + contextId = 2L, + contextType = "Course" + ) + ), + ) + + fileFolderDao.insertAll(folders) + + val remoteFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "file1", folderId = 1L, updatedDate = Date(2))), + FileFolderEntity(FileFolder(id = 4L, name = "file2", folderId = 1L, updatedDate = Date(1))), + FileFolderEntity(FileFolder(id = 5L, name = "file3", folderId = 2L, updatedDate = Date(1000))), + FileFolderEntity(FileFolder(id = 6L, name = "file4", folderId = 2L, updatedDate = Date(1000))) + ) + + fileFolderDao.insertAll(remoteFiles) + + val result = fileFolderDao.findFilesToSync(1L, true) + + assertEquals(listOf(remoteFiles[0]), result) + } + + @Test + fun testFindFilesToSyncNoLocalFiles() = runTest { + courseDao.insert(CourseEntity(Course(id = 1L, name = "course1"))) + courseDao.insert(CourseEntity(Course(id = 2L, name = "course2"))) + + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + name = "folder2", + parentFolderId = 0, + contextId = 2L, + contextType = "Course" + ) + ), + ) + + fileFolderDao.insertAll(folders) + + val remoteFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "file1", folderId = 1L, createdDate = Date(1))), + FileFolderEntity(FileFolder(id = 4L, name = "file2", folderId = 1L, createdDate = Date(1))), + FileFolderEntity(FileFolder(id = 5L, name = "file3", folderId = 2L, createdDate = Date(1000))), + FileFolderEntity(FileFolder(id = 6L, name = "file4", folderId = 2L, createdDate = Date(1000))) + ) + + fileFolderDao.insertAll(remoteFiles) + + val result = fileFolderDao.findFilesToSync(2L, true) + + assertEquals(remoteFiles.subList(2, 4), result) + } + + @Test + fun testFindFilesToSyncSyncSettingsNoLocalFiles() = runTest { + courseDao.insert(CourseEntity(Course(id = 1L, name = "course1"))) + + val courseSyncSettings = CourseSyncSettingsEntity(courseId = 1L, courseName = "Course 1", fullContentSync = true) + courseSyncSettingsDao.insert(courseSyncSettings) + + val fileSyncSettings = FileSyncSettingsEntity(id = 3L, courseId = 1L, fileName = "file1", url = "url1") + + fileSyncSettingsDao.insert(fileSyncSettings) + + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ) + ) + + fileFolderDao.insertAll(folders) + + val remoteFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "file1", folderId = 1L, createdDate = Date(1))), + FileFolderEntity(FileFolder(id = 4L, name = "file2", folderId = 1L, createdDate = Date(1))) + ) + + fileFolderDao.insertAll(remoteFiles) + + val result = fileFolderDao.findFilesToSync(1L, false) + + assertEquals(listOf(remoteFiles[0]), result) + } + + @Test + fun testFindFileToSyncFull() = runTest { + courseDao.insert(CourseEntity(Course(id = 1L, name = "course1"))) + courseDao.insert(CourseEntity(Course(id = 2L, name = "course2"))) + + val localFiles = listOf( + LocalFileEntity(id = 3L, path = "createAt", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 4L, path = "no update", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 5L, path = "updatedAt", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 7L, path = "file4", courseId = 2L, createdDate = Date(1000)), + LocalFileEntity(id = 8L, path = "file5", courseId = 2L, createdDate = Date(1000)) + ) + + localFiles.forEach { localFileDao.insert(it) } + + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + name = "folder2", + parentFolderId = 0, + contextId = 2L, + contextType = "Course" + ) + ), + ) + + fileFolderDao.insertAll(folders) + + val remoteFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "createdAt", folderId = 1L, createdDate = Date(2))), + FileFolderEntity(FileFolder(id = 4L, name = "no update", folderId = 1L, createdDate = Date(1))), + FileFolderEntity(FileFolder(id = 5L, name = "updatedAt", folderId = 1L, updatedDate = Date(2))), + FileFolderEntity(FileFolder(id = 6L, name = "not synced", folderId = 1L, updatedDate = Date(1))), + FileFolderEntity(FileFolder(id = 7L, name = "file4", folderId = 2L, createdDate = Date(1000))), + FileFolderEntity(FileFolder(id = 8L, name = "file5", folderId = 2L, createdDate = Date(1000))) + ) + + fileFolderDao.insertAll(remoteFiles) + + val result = fileFolderDao.findFilesToSync(1L, true) + + assertEquals(listOf(remoteFiles[0], remoteFiles[2], remoteFiles[3]), result) + } + + @Test + fun testFindFileToSyncSelected() = runTest { + courseDao.insert(CourseEntity(Course(id = 1L, name = "course1"))) + courseDao.insert(CourseEntity(Course(id = 2L, name = "course2"))) + + val courseSyncSettings = CourseSyncSettingsEntity(courseId = 1L, courseName = "Course 1", fullContentSync = false) + courseSyncSettingsDao.insert(courseSyncSettings) + + val fileSyncSettings = listOf( + FileSyncSettingsEntity(id = 3L, courseId = 1L, fileName = "file1", url = "url1"), + FileSyncSettingsEntity(id = 4L, courseId = 1L, fileName = "file3", url = "url3"), + FileSyncSettingsEntity(id = 6L, courseId = 1L, fileName = "file4", url = "url4"), + ) + + fileSyncSettingsDao.insertAll(fileSyncSettings) + + val localFiles = listOf( + LocalFileEntity(id = 3L, path = "createAt", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 4L, path = "no update", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 5L, path = "updatedAt", courseId = 1L, createdDate = Date(1)), + LocalFileEntity(id = 7L, path = "file4", courseId = 2L, createdDate = Date(1000)), + LocalFileEntity(id = 8L, path = "file5", courseId = 2L, createdDate = Date(1000)) + ) + + localFiles.forEach { localFileDao.insert(it) } + + val folders = listOf( + FileFolderEntity( + FileFolder( + id = 1L, + name = "folder1", + parentFolderId = 0, + contextId = 1L, + contextType = "Course" + ) + ), + FileFolderEntity( + FileFolder( + id = 2L, + name = "folder2", + parentFolderId = 0, + contextId = 2L, + contextType = "Course" + ) + ), + ) + + fileFolderDao.insertAll(folders) + + val remoteFiles = listOf( + FileFolderEntity(FileFolder(id = 3L, name = "createdAt", folderId = 1L, createdDate = Date(2))), + FileFolderEntity(FileFolder(id = 4L, name = "no update", folderId = 1L, createdDate = Date(1))), + FileFolderEntity(FileFolder(id = 5L, name = "updatedAt", folderId = 1L, updatedDate = Date(2))), + FileFolderEntity(FileFolder(id = 6L, name = "not synced", folderId = 1L, updatedDate = Date(1))), + FileFolderEntity(FileFolder(id = 7L, name = "file4", folderId = 2L, createdDate = Date(1000))), + FileFolderEntity(FileFolder(id = 8L, name = "file5", folderId = 2L, createdDate = Date(1000))) + ) + + fileFolderDao.insertAll(remoteFiles) + + val result = fileFolderDao.findFilesToSync(1L, false) + + assertEquals(listOf(remoteFiles[0], remoteFiles[3]), result) + } + + @Test + fun testFindByIds() = runTest { + val files = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "file1")), + FileFolderEntity(FileFolder(id = 2L, name = "file2")), + FileFolderEntity(FileFolder(id = 3L, name = "file3")) + ) + + fileFolderDao.insertAll(files) + + val result = fileFolderDao.findByIds(setOf(1, 2)) + + assertEquals(listOf(files[0], files[1]), result) + } + + @Test + fun testDeleteAllByCourseIdDeleteFilesWhereParentFolderHasCourseId() = runTest { + val files = listOf( + FileFolderEntity(FileFolder(id = 1L, name = "file1", folderId = 1L, contextId = 1L)), + FileFolderEntity(FileFolder(id = 2L, name = "file2", folderId = 1L)), + FileFolderEntity(FileFolder(id = 3L, name = "file2", folderId = 2L)), + ) + + fileFolderDao.insertAll(files) + + fileFolderDao.deleteAllByCourseId(1L) + + val result = fileFolderDao.findByIds(setOf(1, 2, 3)) + + assertEquals(listOf(files[2]), result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt new file mode 100644 index 0000000000..16674d61b0 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDaoTest.kt @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class FileSyncProgressDaoTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private lateinit var db: OfflineDatabase + private lateinit var courseSyncProgressDao: CourseSyncProgressDao + private lateinit var fileSyncProgressDao: FileSyncProgressDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + courseSyncProgressDao = db.courseSyncProgressDao() + fileSyncProgressDao = db.fileSyncProgressDao() + + courseSyncProgressDao.insert( + CourseSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + courseName = "Course 1" + ) + ) + } + + @After + fun tearDown() { + db.close() + } + + @Test(expected = SQLiteConstraintException::class) + fun testInsertError() = runTest { + val entity = FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + fileSyncProgressDao.insert(entity) + + val updatedEntity = entity.copy(progressState = ProgressState.COMPLETED) + fileSyncProgressDao.insert(updatedEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testInsertAllError() = runTest { + val entity = FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + fileSyncProgressDao.insertAll(listOf(entity)) + + val updatedEntity = entity.copy(progressState = ProgressState.COMPLETED) + fileSyncProgressDao.insertAll(listOf(updatedEntity)) + } + + @Test(expected = SQLiteConstraintException::class) + fun testForeignKeyInsert() = runTest { + courseSyncProgressDao.deleteAll() + + val entity = FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + fileSyncProgressDao.insert(entity) + } + + @Test + fun testForeignKeyDelete() = runTest { + val entity = FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + fileSyncProgressDao.insert(entity) + + courseSyncProgressDao.deleteAll() + + val result = fileSyncProgressDao.findByCourseId(1L) + + assert(result.isEmpty()) + } + + @Test + fun testFindByWorkerId() = runTest { + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findByWorkerId("workerId") + + assertEquals(entities[0], result) + } + + @Test + fun testFindByFileId() = runTest { + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 2L + ) + ) + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findByFileId(1L) + + assertEquals(entities[0], result) + } + + @Test + fun testFindByWorkerIdLiveData() = runTest { + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findByWorkerIdLiveData("workerId") + result.observeForever { } + + assertEquals(entities[0], result.value) + } + + @Test + fun testFindByCourseIdLiveData() = runTest { + courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "workerId2", "Course 2")) + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId3", + courseId = 2L, + fileName = "File 3", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findByCourseIdLiveData(1L) + result.observeForever { } + + assertEquals(entities.subList(0, 2), result.value) + } + + @Test + fun testFindAllLiveData() = runTest { + courseSyncProgressDao.insert(CourseSyncProgressEntity(2L, "workerId2", "Course 2")) + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId3", + courseId = 2L, + fileName = "File 3", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findAllLiveData() + result.observeForever { } + + assertEquals(entities, result.value) + } + + @Test + fun testDeleteAll() = runTest { + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + + fileSyncProgressDao.insertAll(entities) + + fileSyncProgressDao.deleteAll() + + val result = fileSyncProgressDao.findAllLiveData() + result.observeForever { } + + assert(result.value!!.isEmpty()) + } + + @Test + fun testFindAdditionalFilesByCourseIdLiveDataTest() = runTest { + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId3", + courseId = 1L, + fileName = "File 3", + progress = 0, + fileSize = 1000L, + additionalFile = true, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId4", + courseId = 1L, + fileName = "File 3", + progress = 0, + fileSize = 0, + additionalFile = true, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findAdditionalFilesByCourseIdLiveData(1L) + result.observeForever { } + + assertEquals(entities.subList(2, 4), result.value) + } + + @Test + fun testFindCourseFilesByCourseIdLiveData() = runTest { + val entities = listOf( + FileSyncProgressEntity( + workerId = "workerId", + courseId = 1L, + fileName = "File 1", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId2", + courseId = 1L, + fileName = "File 2", + progress = 0, + fileSize = 1000L, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId3", + courseId = 1L, + fileName = "File 3", + progress = 0, + fileSize = 1000L, + additionalFile = true, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ), + FileSyncProgressEntity( + workerId = "workerId4", + courseId = 1L, + fileName = "File 3", + progress = 0, + fileSize = 0, + additionalFile = true, + progressState = ProgressState.IN_PROGRESS, + fileId = 1L + ) + ) + + fileSyncProgressDao.insertAll(entities) + + val result = fileSyncProgressDao.findCourseFilesByCourseIdLiveData(1L) + result.observeForever { } + + assertEquals(entities.subList(0, 2), result.value) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncSettingsDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncSettingsDaoTest.kt new file mode 100644 index 0000000000..4034a45d7c --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/FileSyncSettingsDaoTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class FileSyncSettingsDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var fileSyncSettingsDao: FileSyncSettingsDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + fileSyncSettingsDao = db.fileSyncSettingsDao() + + val courseSyncSettingsDao = db.courseSyncSettingsDao() + courseSyncSettingsDao.insert( + CourseSyncSettingsEntity( + courseId = 1L, + courseName = "Course 1", + fullContentSync = false + ) + ) + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplaces() = runTest { + val fileSyncSettingsEntity = FileSyncSettingsEntity(1L, "", 1L, null) + + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + + val updated = fileSyncSettingsEntity.copy(url = "https://instructure.com") + + fileSyncSettingsDao.insert(updated) + + val result = fileSyncSettingsDao.findById(1L) + + assertEquals(updated, result) + } + + @Test + fun testFindById() = runTest { + val fileSyncSettingsEntity = FileSyncSettingsEntity(1L, "", 1L, null) + val fileSyncSettingsEntity2 = FileSyncSettingsEntity(2L, "", 1L, null) + + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + fileSyncSettingsDao.insert(fileSyncSettingsEntity2) + + val result = fileSyncSettingsDao.findById(2L) + + assertEquals(fileSyncSettingsEntity2, result) + } + + @Test + fun testFindAll() = runTest { + val fileSyncSettingsEntity = FileSyncSettingsEntity(1L, "", 1L, null) + val fileSyncSettingsEntity2 = FileSyncSettingsEntity(2L, "", 1L, null) + + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + fileSyncSettingsDao.insert(fileSyncSettingsEntity2) + + val result = fileSyncSettingsDao.findAll() + + assertEquals(listOf(fileSyncSettingsEntity, fileSyncSettingsEntity2), result) + } + + @Test + fun testDeleteById() = runTest { + val fileSyncSettingsEntity = FileSyncSettingsEntity(1L, "", 1L, null) + val fileSyncSettingsEntity2 = FileSyncSettingsEntity(2L, "", 1L, null) + + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + fileSyncSettingsDao.insert(fileSyncSettingsEntity2) + + val result1 = fileSyncSettingsDao.findById(1L) + assertEquals(fileSyncSettingsEntity, result1) + + fileSyncSettingsDao.deleteById(1L) + + val result2 = fileSyncSettingsDao.findById(1L) + assertNull(result2) + } + + @Test + fun testDeleteByIds() = runTest { + val fileSyncSettingsEntity = FileSyncSettingsEntity(1L, "", 1L, null) + val fileSyncSettingsEntity2 = FileSyncSettingsEntity(2L, "", 1L, null) + val fileSyncSettingsEntity3 = FileSyncSettingsEntity(3L, "", 1L, null) + + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + fileSyncSettingsDao.insert(fileSyncSettingsEntity2) + fileSyncSettingsDao.insert(fileSyncSettingsEntity3) + + val result1 = fileSyncSettingsDao.findById(1L) + assertEquals(fileSyncSettingsEntity, result1) + val result2 = fileSyncSettingsDao.findById(3L) + assertEquals(fileSyncSettingsEntity3, result2) + + fileSyncSettingsDao.deleteByIds(listOf(1L, 3L)) + + val result3 = fileSyncSettingsDao.findById(1L) + assertNull(result3) + val result4 = fileSyncSettingsDao.findById(3L) + assertNull(result4) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt new file mode 100644 index 0000000000..e32f43a63c --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDaoTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.GradingPeriodEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class GradingPeriodDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var gradingPeriodDao: GradingPeriodDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + gradingPeriodDao = db.gradingPeriodDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityById() = runTest { + val gradingPeriodEntity = GradingPeriodEntity(GradingPeriod(id = 1, "Grading period 1")) + val gradingPeriodEntity2 = GradingPeriodEntity(GradingPeriod(id = 2, "Grading period 2")) + gradingPeriodDao.insert(gradingPeriodEntity) + gradingPeriodDao.insert(gradingPeriodEntity2) + + val result = gradingPeriodDao.findById(1) + + Assert.assertEquals(gradingPeriodEntity, result) + } + + @Test + fun testFindEntityByIdReturnsNullIfNotFound() = runTest { + val gradingPeriodEntity = GradingPeriodEntity(GradingPeriod(id = 1, "Grading period 1")) + val gradingPeriodEntity2 = GradingPeriodEntity(GradingPeriod(id = 2, "Grading period 2")) + gradingPeriodDao.insert(gradingPeriodEntity) + gradingPeriodDao.insert(gradingPeriodEntity2) + + val result = gradingPeriodDao.findById(3) + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GroupUserDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GroupUserDaoTest.kt new file mode 100644 index 0000000000..0767164288 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/GroupUserDaoTest.kt @@ -0,0 +1,101 @@ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.GroupEntity +import com.instructure.pandautils.room.offline.entities.GroupUserEntity +import com.instructure.pandautils.room.offline.entities.UserEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class GroupUserDaoTest { + private lateinit var db: OfflineDatabase + + private lateinit var groupUserDao: GroupUserDao + private lateinit var groupDao: GroupDao + private lateinit var userDao: UserDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + groupUserDao = db.groupUserDao() + groupDao = db.groupDao() + userDao = db.userDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + groupDao.insert(GroupEntity(Group(1L))) + groupDao.insert(GroupEntity(Group(2L))) + userDao.insert(UserEntity(User(1L))) + + val groupUserEntity = GroupUserEntity(1L, 1L) + val id = groupUserDao.insert(groupUserEntity) + + val updated = groupUserEntity.copy(id = id, groupId = 2L) + groupUserDao.insert(updated) + + val result = groupUserDao.findByUserId(1L) + + Assert.assertEquals(updated.groupId, result?.get(0)) + } + + @Test + fun testInsertAllReplace() = runTest { + groupDao.insert(GroupEntity(Group(1L))) + groupDao.insert(GroupEntity(Group(2L))) + userDao.insert(UserEntity(User(1L))) + userDao.insert(UserEntity(User(2L))) + + val discussionEntryEntities = listOf( + GroupUserEntity(1L, 1L), + GroupUserEntity(1L, 2L) + ) + val ids = groupUserDao.insertAll(discussionEntryEntities) + + val updated = discussionEntryEntities.mapIndexed { index, entity -> entity.copy(id = ids[index], groupId = 2L) } + groupUserDao.insertAll(updated) + + val result0 = groupUserDao.findByUserId(1L) + val result1 = groupUserDao.findByUserId(2L) + + Assert.assertEquals(updated[0].groupId, result0?.get(0)) + Assert.assertEquals(updated[1].groupId, result1?.get(0)) + } + + @Test + fun testFindByUserId() = runTest { + groupDao.insert(GroupEntity(Group(1L))) + groupDao.insert(GroupEntity(Group(2L))) + userDao.insert(UserEntity(User(1L))) + userDao.insert(UserEntity(User(2L))) + + val discussionEntryEntities = listOf( + GroupUserEntity(1L, 1L), + GroupUserEntity(2L, 2L) + ) + groupUserDao.insertAll(discussionEntryEntities) + + val result = groupUserDao.findByUserId(2L) + + Assert.assertEquals(discussionEntryEntities[1].groupId, result?.get(0)) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LocalFileDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LocalFileDaoTest.kt new file mode 100644 index 0000000000..abd7d8ac51 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LocalFileDaoTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import com.instructure.canvasapi2.models.Course +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.util.Date + +@ExperimentalCoroutinesApi +class LocalFileDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var localFileDao: LocalFileDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + localFileDao = db.localFileDao() + courseDao = db.courseDao() + + courseDao.insert(CourseEntity(Course(1L))) + } + + @Test + fun testInsertReplace() = runTest { + val file = LocalFileEntity(1L, 1L, Date(), "") + val updated = LocalFileEntity(1L, 1L, Date(), "updated") + + localFileDao.insert(file) + localFileDao.insert(updated) + + val result = localFileDao.findById(1L) + + assertEquals(updated, result) + } + + @Test + fun testFindById() = runTest { + val files = listOf( + LocalFileEntity(1L, 1L, Date(), ""), + LocalFileEntity(2L, 1L, Date(), "") + ) + + files.forEach { + localFileDao.insert(it) + } + + val result = localFileDao.findById(1L) + + assertEquals(files[0], result) + } + + @Test + fun testFindByIds() = runTest { + val files = listOf( + LocalFileEntity(1L, 1L, Date(), ""), + LocalFileEntity(2L, 1L, Date(), "") + ) + + files.forEach { + localFileDao.insert(it) + } + + val result = localFileDao.findByIds(listOf(1L, 2L)) + + assertEquals(files, result) + } + + @Test + fun testFindRemovedFiles() = runTest { + val course2 = CourseEntity(Course(2L)) + + courseDao.insert(course2) + + val files = listOf( + LocalFileEntity(1L, 1L, Date(), ""), + LocalFileEntity(2L, 1L, Date(), ""), + LocalFileEntity(3L, 1L, Date(), ""), + LocalFileEntity(4L, 2L, Date(), "") + ) + + files.forEach { + localFileDao.insert(it) + } + + val result = localFileDao.findRemovedFiles(1L, listOf(1L, 3L, 4L)) + + assertEquals(listOf(files[1]), result) + } + + @Test + fun testFindByCourseId() = runTest { + val files = listOf( + LocalFileEntity(3L, 1L, Date(), ""), + LocalFileEntity(4L, 2L, Date(), "") + ) + + files.forEach { + localFileDao.insert(it) + } + + val result = localFileDao.findByCourseId(1L) + + assertEquals(files.take(1), result) + } + + @Test + fun testExistsById() = runTest { + localFileDao.insert(LocalFileEntity(1L, 1L, Date(), "")) + + val existsResult = localFileDao.existsById(1L) + val notExistsResult = localFileDao.existsById(2L) + + assertEquals(true, existsResult) + assertEquals(false, notExistsResult) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt new file mode 100644 index 0000000000..e9107c983f --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockInfoDaoTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class LockInfoDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var lockInfoDao: LockInfoDao + private lateinit var courseDao: CourseDao + private lateinit var pageDao: PageDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var moduleContentDetailsDao: ModuleContentDetailsDao + private lateinit var moduleItemDao: ModuleItemDao + private lateinit var moduleObjectDao: ModuleObjectDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + lockInfoDao = db.lockInfoDao() + courseDao = db.courseDao() + pageDao = db.pageDao() + assignmentGroupDao = db.assignmentGroupDao() + assignmentDao = db.assignmentDao() + moduleContentDetailsDao = db.moduleContentDetailsDao() + moduleItemDao = db.moduleItemDao() + moduleObjectDao = db.moduleObjectDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + pageDao.insert(PageEntity(Page(1L), 1L)) + pageDao.insert(PageEntity(Page(2L), 1L)) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + assignmentDao.insert(AssignmentEntity(Assignment(2L, assignmentGroupId = 1L), null, null, null, null)) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(1L), 1L)) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(1L), 1L)) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(2L), 1L)) + moduleContentDetailsDao.insert(ModuleContentDetailsEntity(ModuleContentDetails("Points"), 1L)) + moduleContentDetailsDao.insert(ModuleContentDetailsEntity(ModuleContentDetails("Points 2"), 2L)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByModuleId() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), moduleId = 1)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "2"), moduleId = 2)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "3"), assignmentId = 1)) + + val result = lockInfoDao.findByModuleId(1) + + Assert.assertEquals("1", result!!.unlockAt) + } + + @Test + fun testFindByAssignmentId() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), moduleId = 1)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "2"), moduleId = 2)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "3"), assignmentId = 1)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "4"), assignmentId = 2)) + + val result = lockInfoDao.findByAssignmentId(2) + + Assert.assertEquals("4", result!!.unlockAt) + } + + @Test + fun testFindByPageId() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), moduleId = 1)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "2"), moduleId = 2)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "3"), assignmentId = 1)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "4"), assignmentId = 2)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "5"), pageId = 1)) + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "6"), pageId = 2)) + + val result = lockInfoDao.findByPageId(2) + + Assert.assertEquals("6", result!!.unlockAt) + } + + @Test(expected = SQLiteConstraintException::class) + fun testModuleForeignKey() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), moduleId = 3)) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentForeignKey() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), assignmentId = 3)) + } + + @Test(expected = SQLiteConstraintException::class) + fun testPageForeignKey() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), pageId = 3)) + } + + @Test + fun testModuleCascade() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), moduleId = 1)) + + moduleContentDetailsDao.delete(ModuleContentDetailsEntity(ModuleContentDetails("Points"), 1L)) + + val result = lockInfoDao.findByModuleId(1) + + Assert.assertNull(result) + } + + @Test + fun testAssignmentCascade() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), assignmentId = 1)) + + assignmentDao.delete(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + + val result = lockInfoDao.findByModuleId(1) + + Assert.assertNull(result) + } + + @Test + fun testPageCascade() = runTest { + lockInfoDao.insert(LockInfoEntity(LockInfo(unlockAt = "1"), pageId = 1)) + + pageDao.delete(PageEntity(Page(1L), 1L)) + + val result = lockInfoDao.findByModuleId(1) + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt new file mode 100644 index 0000000000..aac575c8c5 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/LockedModuleDaoTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class LockedModuleDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var lockedModuleDao: LockedModuleDao + private lateinit var courseDao: CourseDao + private lateinit var pageDao: PageDao + private lateinit var lockInfoDao: LockInfoDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + lockedModuleDao = db.lockedModuleDao() + courseDao = db.courseDao() + pageDao = db.pageDao() + lockInfoDao = db.lockInfoDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + pageDao.insert(PageEntity(Page(1L), 1L)) + lockInfoDao.insert(LockInfoEntity(LockInfo(contextModule = LockedModule(1L)), pageId = 1L)) + lockInfoDao.insert(LockInfoEntity(LockInfo(contextModule = LockedModule(2L)), pageId = 1L)) + lockInfoDao.insert(LockInfoEntity(LockInfo(contextModule = LockedModule(3L)), pageId = 1L)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + val expected = LockedModuleEntity(LockedModule(id = 1)) + + lockedModuleDao.insert(expected) + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 2))) + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 3))) + + val result = lockedModuleDao.findById(1) + + Assert.assertEquals(expected, result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testLockInfoForeignKey() = runTest { + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 4))) + } + + @Test + fun testLockInfoCascade() = runTest { + val id = lockInfoDao.insert(LockInfoEntity(LockInfo(contextModule = LockedModule(1)))) + + lockedModuleDao.insert(LockedModuleEntity(LockedModule(id = 1))) + + lockInfoDao.delete(LockInfoEntity(id, null, null, null, null, null, null)) + + val result = lockedModuleDao.findById(1) + + Assert.assertNull(result) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/MasteryPathAssignmentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/MasteryPathAssignmentDaoTest.kt new file mode 100644 index 0000000000..64ad23cfe5 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/MasteryPathAssignmentDaoTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.AssignmentSet +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.MasteryPath +import com.instructure.canvasapi2.models.MasteryPathAssignment +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathAssignmentEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class MasteryPathAssignmentDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var masteryPathAssignmentDao: MasteryPathAssignmentDao + private lateinit var assignmentSetDao: AssignmentSetDao + private lateinit var masteryPathDao: MasteryPathDao + private lateinit var moduleItemDao: ModuleItemDao + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + masteryPathAssignmentDao = db.masteryPathAssignmentDao() + assignmentSetDao = db.assignmentSetDao() + masteryPathDao = db.masteryPathDao() + moduleItemDao = db.moduleItemDao() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByAssignmentSetId() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 2), 1 )) + + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 1)) + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 2)) + + assignmentSetDao.insert(AssignmentSetEntity(AssignmentSet(id = 1), 1)) + assignmentSetDao.insert(AssignmentSetEntity(AssignmentSet(id = 2), 2)) + + masteryPathAssignmentDao.insert( + MasteryPathAssignmentEntity(MasteryPathAssignment(id = 1, assignmentSetId = 1)) + ) + + masteryPathAssignmentDao.insert( + MasteryPathAssignmentEntity(MasteryPathAssignment(id = 2, assignmentSetId = 2)) + ) + + val result = masteryPathAssignmentDao.findByAssignmentSetId(1) + + Assert.assertEquals(1, result.size) + Assert.assertEquals(1, result[0].id) + } + + @Test + fun testCascadeWhenAssignmentSetIsDeleted() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 2), 1 )) + + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 1)) + masteryPathDao.insert(MasteryPathEntity(MasteryPath(), 2)) + + assignmentSetDao.insert(AssignmentSetEntity(AssignmentSet(id = 1), 1)) + assignmentSetDao.insert(AssignmentSetEntity(AssignmentSet(id = 2), 2)) + + masteryPathAssignmentDao.insert( + MasteryPathAssignmentEntity(MasteryPathAssignment(id = 1, assignmentSetId = 1)) + ) + + masteryPathAssignmentDao.insert( + MasteryPathAssignmentEntity(MasteryPathAssignment(id = 2, assignmentSetId = 2)) + ) + + val result = masteryPathAssignmentDao.findByAssignmentSetId(1) + Assert.assertEquals(1, result.size) + Assert.assertEquals(1, result[0].id) + + assignmentSetDao.delete(AssignmentSetEntity(AssignmentSet(id = 1), 1)) + + val resultAferDelete = masteryPathAssignmentDao.findByAssignmentSetId(1) + Assert.assertEquals(0, resultAferDelete.size) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/MasteryPathDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/MasteryPathDaoTest.kt new file mode 100644 index 0000000000..d6e3f3d715 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/MasteryPathDaoTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.MasteryPath +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class MasteryPathDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var masteryPathDao: MasteryPathDao + private lateinit var moduleItemDao: ModuleItemDao + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + masteryPathDao = db.masteryPathDao() + moduleItemDao = db.moduleItemDao() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 2), 1 )) + + masteryPathDao.insert(MasteryPathEntity(MasteryPath(isLocked = true), 1)) + masteryPathDao.insert(MasteryPathEntity(MasteryPath(isLocked = false), 2)) + + val result = masteryPathDao.findById(1) + + Assert.assertEquals(1L, result?.id) + Assert.assertTrue(result!!.isLocked) + } + + @Test(expected = SQLiteConstraintException::class) + fun testModuleItemForeignKeyRequired() = runTest { + masteryPathDao.insert(MasteryPathEntity(MasteryPath(isLocked = true), 1)) + } + + @Test + fun testCascadeWhenModuleItemDeleted() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + + masteryPathDao.insert(MasteryPathEntity(MasteryPath(isLocked = true), 1)) + + val result = masteryPathDao.findById(1) + Assert.assertEquals(1L, result?.id) + Assert.assertTrue(result!!.isLocked) + + moduleItemDao.delete(ModuleItemEntity(ModuleItem(id = 1), 1 )) + + val resultAfterDelete = masteryPathDao.findById(1) + Assert.assertNull(resultAfterDelete) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt new file mode 100644 index 0000000000..4017a2d369 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDaoTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ModuleCompletionRequirementDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var moduleCompletionRequirementDao: ModuleCompletionRequirementDao + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + moduleCompletionRequirementDao = db.moduleCompletionRequirementDao() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByModuleId() = runTest { + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 2), 1)) + + moduleCompletionRequirementDao.insert( + ModuleCompletionRequirementEntity( + ModuleCompletionRequirement(id = 1, minScore = 10.0), 1 + ) + ) + + moduleCompletionRequirementDao.insert( + ModuleCompletionRequirementEntity( + ModuleCompletionRequirement(id = 2, minScore = 20.0), 2 + ) + ) + + val result = moduleCompletionRequirementDao.findByModuleId(1) + + Assert.assertEquals(1, result.size) + Assert.assertEquals(1L, result[0].id) + Assert.assertEquals(10.0, result[0].minScore, 0.0) + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleCompletionRequirementDao.insert( + ModuleCompletionRequirementEntity( + ModuleCompletionRequirement(id = 1, minScore = 10.0), 1 + ) + ) + + moduleCompletionRequirementDao.insert( + ModuleCompletionRequirementEntity( + ModuleCompletionRequirement(id = 2, minScore = 20.0), 1 + ) + ) + + val result = moduleCompletionRequirementDao.findById(1) + + Assert.assertEquals(1L, result!!.id) + Assert.assertEquals(10.0, result.minScore, 0.0) + } + + @Test(expected = SQLiteConstraintException::class) + fun testModuleItemForeignKey() = runTest { + moduleCompletionRequirementDao.insert( + ModuleCompletionRequirementEntity(ModuleCompletionRequirement(id = 1, minScore = 10.0), 2) + ) + } + + @Test + fun testModuleItemCascade() = runTest { + moduleCompletionRequirementDao.insert( + ModuleCompletionRequirementEntity(ModuleCompletionRequirement(id = 1, minScore = 10.0), 1) + ) + + moduleObjectDao.delete(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val result = moduleCompletionRequirementDao.findByModuleId(1) + + Assert.assertTrue(result.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleContentDetailsDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleContentDetailsDaoTest.kt new file mode 100644 index 0000000000..05ed976f18 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleContentDetailsDaoTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleContentDetails +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.ModuleContentDetailsEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ModuleContentDetailsDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var moduleContentDetailsDao: ModuleContentDetailsDao + private lateinit var moduleItemDao: ModuleItemDao + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + moduleContentDetailsDao = db.moduleContentDetailsDao() + moduleItemDao = db.moduleItemDao() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 2), 1 )) + + moduleContentDetailsDao.insert( + ModuleContentDetailsEntity(ModuleContentDetails(pointsPossible = "10"), 1)) + moduleContentDetailsDao.insert( + ModuleContentDetailsEntity(ModuleContentDetails(pointsPossible = "20"), 2)) + + val result = moduleContentDetailsDao.findById(1) + + Assert.assertEquals(1L, result?.id) + Assert.assertEquals("10", result?.pointsPossible) + } + + @Test + fun testCascadeWhenModuleItemDeleted() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + + moduleContentDetailsDao.insert( + ModuleContentDetailsEntity(ModuleContentDetails(pointsPossible = "10"), 1)) + + val result = moduleContentDetailsDao.findById(1) + Assert.assertEquals(1L, result?.id) + + moduleItemDao.delete(ModuleItemEntity(ModuleItem(id = 1), 1 )) + + val resultAfterDelete = moduleContentDetailsDao.findById(1) + Assert.assertNull(resultAfterDelete) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleItemDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleItemDaoTest.kt new file mode 100644 index 0000000000..e538266d01 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleItemDaoTest.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ModuleItemDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var moduleItemDao: ModuleItemDao + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + moduleItemDao = db.moduleItemDao() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByModuleIdAndOrderByPosition() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 2), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1), 1), + ModuleItemEntity(ModuleItem(id = 2, position = 1), 2), + ModuleItemEntity(ModuleItem(id = 3, position = 3), 2), + ModuleItemEntity(ModuleItem(id = 4, position = 2), 2) + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findByModuleId(2) + + Assert.assertEquals(3, result.size) + Assert.assertEquals(2L, result[0].id) + Assert.assertEquals(4L, result[1].id) + Assert.assertEquals(3L, result[2].id) + } + + @Test + fun testCascadeWhenModuleObjectDeleted() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + moduleItemDao.insert(ModuleItemEntity(ModuleItem(id = 1), 1 )) + + val result = moduleItemDao.findByModuleId(1) + Assert.assertEquals(1, result.size) + + moduleObjectDao.delete(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val resultAfterDelete = moduleItemDao.findByModuleId(1) + Assert.assertEquals(0, resultAfterDelete.size) + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1), 1), + ModuleItemEntity(ModuleItem(id = 2), 1), + ModuleItemEntity(ModuleItem(id = 3), 1), + ModuleItemEntity(ModuleItem(id = 55, title = "This is the way"), 1), + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findById(55) + + Assert.assertEquals(55, result!!.id) + Assert.assertEquals("This is the way", result.title) + } + + @Test + fun testFindByIdReturnsNullWhenNotFound() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1), 1), + ModuleItemEntity(ModuleItem(id = 2), 1), + ModuleItemEntity(ModuleItem(id = 3), 1), + ModuleItemEntity(ModuleItem(id = 55, title = "This is the way"), 1), + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findById(14) + + Assert.assertNull(result) + } + + @Test + fun testFindByTypeAndContentId() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1, type = "Quiz", contentId = 1), 1), + ModuleItemEntity(ModuleItem(id = 2, type = "Assignment", contentId = 1), 1), + ModuleItemEntity(ModuleItem(id = 3, type = "Quiz", contentId = 2), 1), + ModuleItemEntity(ModuleItem(id = 55, type = "Assignment", contentId = 2, title = "This is the way"), 1), + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findByTypeAndContentId("Assignment", 2) + + Assert.assertEquals(55, result!!.id) + Assert.assertEquals("This is the way", result.title) + Assert.assertEquals("Assignment", result.type) + Assert.assertEquals(2, result.contentId) + } + + @Test + fun testFindByTypeAndContentIdReturnsNullWhenNotFound() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1, type = "Quiz", contentId = 1), 1), + ModuleItemEntity(ModuleItem(id = 2, type = "Assignment", contentId = 1), 1), + ModuleItemEntity(ModuleItem(id = 3, type = "Quiz", contentId = 2), 1), + ModuleItemEntity(ModuleItem(id = 55, type = "Assignment", contentId = 2, title = "This is the way"), 1), + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findByTypeAndContentId("Assignment", 14) + + Assert.assertNull(result) + } + + @Test + fun testFindByPageUrl() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1, pageUrl = "github.com/hermannakos"), 1), + ModuleItemEntity(ModuleItem(id = 2, pageUrl = "github.com/kristofnemere"), 1), + ModuleItemEntity(ModuleItem(id = 3, pageUrl = "github.com/tamaskozmer"), 1), + ModuleItemEntity(ModuleItem(id = 55, pageUrl = "github.com/kozmi55", title = "This is the way"), 1), + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findByPageUrl("github.com/kozmi55") + + Assert.assertEquals(55, result!!.id) + Assert.assertEquals("This is the way", result.title) + Assert.assertEquals("github.com/kozmi55", result.pageUrl) + } + + @Test + fun testFindByPageUrlReturnsNullWhenNotFound() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val entities = listOf( + ModuleItemEntity(ModuleItem(id = 1, pageUrl = "github.com/hermannakos"), 1), + ModuleItemEntity(ModuleItem(id = 2, pageUrl = "github.com/kristofnemere"), 1), + ModuleItemEntity(ModuleItem(id = 3, pageUrl = "github.com/tamaskozmer"), 1), + ModuleItemEntity(ModuleItem(id = 55, pageUrl = "github.com/kozmi55", title = "This is the way"), 1), + ) + + moduleItemDao.insertAll(entities) + + val result = moduleItemDao.findByPageUrl("github.com/thisisnotagithubuser") + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleObjectDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleObjectDaoTest.kt new file mode 100644 index 0000000000..33fae78df6 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ModuleObjectDaoTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ModuleObjectDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var moduleObjectDao: ModuleObjectDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + moduleObjectDao = db.moduleObjectDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByCourseIdAndOrderByPosition() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + courseDao.insert(CourseEntity(Course(id = 2))) + + val entities = listOf( + ModuleObjectEntity(ModuleObject(id = 1), 1), + ModuleObjectEntity(ModuleObject(id = 2, position = 1), 2), + ModuleObjectEntity(ModuleObject(id = 3, position = 3), 2), + ModuleObjectEntity(ModuleObject(id = 4, position = 2), 2) + ) + + moduleObjectDao.insertAll(entities) + + val result = moduleObjectDao.findByCourseId(2) + + Assert.assertEquals(3, result.size) + Assert.assertEquals(2L, result[0].id) + Assert.assertEquals(4L, result[1].id) + Assert.assertEquals(3L, result[2].id) + } + + @Test + fun testCascadeWhenModuleObjectDeleted() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + moduleObjectDao.insert(ModuleObjectEntity(ModuleObject(id = 1), 1)) + + val result = moduleObjectDao.findByCourseId(1) + Assert.assertEquals(1, result.size) + + courseDao.delete(CourseEntity(Course(id = 1))) + + val resultAfterDelete = moduleObjectDao.findByCourseId(1) + Assert.assertEquals(0, resultAfterDelete.size) + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + + val entities = listOf( + ModuleObjectEntity(ModuleObject(id = 1), 1), + ModuleObjectEntity(ModuleObject(id = 2, position = 1), 1), + ModuleObjectEntity(ModuleObject(id = 3, position = 3), 1), + ModuleObjectEntity(ModuleObject(id = 4, position = 2), 1) + ) + + moduleObjectDao.insertAll(entities) + + val result = moduleObjectDao.findById(2) + + Assert.assertEquals(2, result!!.id) + Assert.assertEquals(1, result.position) + Assert.assertEquals(1, result.courseId) + } + + @Test + fun testFindByIdReturnsNullWhenNotFound() = runTest { + courseDao.insert(CourseEntity(Course(id = 1))) + + val entities = listOf( + ModuleObjectEntity(ModuleObject(id = 1), 1), + ModuleObjectEntity(ModuleObject(id = 2, position = 1), 1), + ModuleObjectEntity(ModuleObject(id = 3, position = 3), 1), + ModuleObjectEntity(ModuleObject(id = 4, position = 2), 1) + ) + + moduleObjectDao.insertAll(entities) + + val result = moduleObjectDao.findById(55) + + Assert.assertNull(result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt new file mode 100644 index 0000000000..2749734a27 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/PageDaoTest.kt @@ -0,0 +1,178 @@ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Page +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.PageEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class PageDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var pageDao: PageDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + pageDao = db.pageDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplaces() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1"), courseId = 1L) + pageDao.insert(pageEntity) + + val updated = pageEntity.copy(title = "Update page") + pageDao.insert(updated) + + val result = pageDao.findById(1L) + + assertEquals(updated, result) + } + + @Test + fun testFindAll() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1"), courseId = 1L) + val pageEntity2 = PageEntity(Page(id = 2, title = "Page2"), courseId = 1L) + pageDao.insert(pageEntity) + pageDao.insert(pageEntity2) + + val result = pageDao.findAll() + + assertEquals(listOf(pageEntity, pageEntity2), result) + } + + @Test + fun testFindById() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1"), courseId = 1L) + val pageEntity2 = PageEntity(Page(id = 2, title = "Page2"), courseId = 1L) + pageDao.insert(pageEntity) + pageDao.insert(pageEntity2) + + val result = pageDao.findById(2L) + + assertEquals(pageEntity2, result) + } + + @Test + fun testFindByUrl() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1", url = "page-1-url"), courseId = 1L) + val pageEntity2 = PageEntity(Page(id = 2, title = "Page2", url = "page-2-url"), courseId = 1L) + pageDao.insert(pageEntity) + pageDao.insert(pageEntity2) + + val result = pageDao.findByUrl("page-2-url") + + assertEquals(pageEntity2, result) + } + + @Test + fun testFindFrontPage() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1", frontPage = true), courseId = 1L) + val pageEntity2 = PageEntity(Page(id = 2, title = "Page2"), courseId = 1L) + pageDao.insert(pageEntity) + pageDao.insert(pageEntity2) + + val result = pageDao.getFrontPage(1L) + + assertEquals(pageEntity, result) + } + + @Test + fun testFindByCourseId() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + val courseEntity2 = CourseEntity(Course(id = 2L)) + courseDao.insert(courseEntity) + courseDao.insert(courseEntity2) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1"), courseId = 1L) + val pageEntity2 = PageEntity(Page(id = 2, title = "Page2"), courseId = 1L) + val pageEntity3 = PageEntity(Page(id = 3, title = "Page3"), courseId = 2L) + val pageEntity4 = PageEntity(Page(id = 4, title = "Page4"), courseId = 2L) + pageDao.insert(pageEntity) + pageDao.insert(pageEntity2) + pageDao.insert(pageEntity3) + pageDao.insert(pageEntity4) + + val result1 = pageDao.findByCourseId(1L) + assertEquals(listOf(pageEntity, pageEntity2), result1) + + val result2 = pageDao.findByCourseId(2L) + assertEquals(listOf(pageEntity3, pageEntity4), result2) + } + + @Test + fun testFindPageDetails() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1", url = "url"), courseId = 1L) + val pageEntity2 = PageEntity(Page(id = 2, title = "Page2"), courseId = 1L) + val pageEntity3 = PageEntity(Page(id = 3, title = "Page3"), courseId = 1L) + pageDao.insert(pageEntity) + pageDao.insert(pageEntity2) + pageDao.insert(pageEntity3) + + val resultByUrl = pageDao.getPageDetails(1L, "url") + assertEquals(pageEntity, resultByUrl) + + val resultByTitle = pageDao.getPageDetails(1L, "Page2") + assertEquals(pageEntity2, resultByTitle) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + val courseEntity = CourseEntity(Course(id = 1L)) + courseDao.insert(courseEntity) + + val pageEntity = PageEntity(Page(id = 1, title = "Page1"), courseId = 1L) + pageDao.insert(pageEntity) + + val result = pageDao.findByCourseId(1L) + + assertEquals(listOf(pageEntity), result) + + pageDao.deleteAllByCourseId(1L) + + val deletedResult = pageDao.findByCourseId(1L) + + Assert.assertTrue(deletedResult.isEmpty()) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/QuizDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/QuizDaoTest.kt new file mode 100644 index 0000000000..abec86057c --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/QuizDaoTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Quiz +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.QuizEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class QuizDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var quizDao: QuizDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + quizDao = db.quizDao() + courseDao = db.courseDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val quizEntities = listOf(Quiz(id = 1L, title = "Quiz 1"), Quiz(id = 2L, title = "Quiz 2")).map { QuizEntity(it, 1L) } + val expectedQuizEntity = quizEntities[1] + + quizEntities.forEach { + quizDao.insert(it) + } + + val result = quizDao.findById(2L) + + assertEquals(expectedQuizEntity.title, result?.title) + } + + @Test + fun testFindByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + courseDao.insert(CourseEntity(Course(2L))) + val quizzes = listOf(Quiz(id = 1L, title = "Quiz 1"), Quiz(id = 2L, title = "Quiz 2"), Quiz(id = 3L, title = "Quiz 3")) + val quizEntities = listOf(QuizEntity(quizzes[0], 2L), QuizEntity(quizzes[1], 1L), QuizEntity(quizzes[2], 2L)) + val expectedQuizEntities = quizEntities.filter { it.courseId == 2L } + + quizEntities.forEach { quizDao.insert(it) } + + val result = quizDao.findByCourseId(2L) + assertEquals(expectedQuizEntities.map { it.title }, result.map { it.title }) + } + + @Test + fun testInsertReplace() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + val quizzes = listOf(Quiz(id = 1L, title = "Quiz 1"), Quiz(id = 1L, title = "Quiz 2")) + val expectedTitle = quizzes[1].title + + quizDao.insert(QuizEntity(quizzes[0], 1L)) + quizDao.insert(QuizEntity(quizzes[1], 1L)) + + val result = quizDao.findById(1L) + + assertEquals(expectedTitle, result?.title) + } + + @Test(expected = SQLiteConstraintException::class) + fun testForeignKeyConstraint() = runTest { + quizDao.insert(QuizEntity(Quiz(id = 1L), 1L)) + } + + @Test + fun testDeleteAndInsertAll() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + + val quizzes = listOf(Quiz(id = 1L, title = "Quiz 1"), Quiz(id = 1L, title = "Quiz 2")).map { QuizEntity(it, 1L) } + quizDao.insertAll(quizzes) + + val expected = listOf(Quiz(id = 3L, title = "Quiz 3"), Quiz(id = 4L, title = "Quiz 4")).map { QuizEntity(it, 1L) } + quizDao.deleteAndInsertAll(expected, 1L) + + val result = quizDao.findByCourseId(1L) + assertEquals(expected, result) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + courseDao.insert(CourseEntity(Course(1L))) + + val quizEntity = QuizEntity(Quiz(id = 1L, title = "Quiz 1"), 1L) + quizDao.insert(quizEntity) + + val result = quizDao.findByCourseId(1L) + + assertEquals(listOf(quizEntity), result) + + quizDao.deleteAllByCourseId(1L) + + val deletedResult = quizDao.findByCourseId(1L) + + Assert.assertTrue(deletedResult.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionAssessmentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionAssessmentDaoTest.kt new file mode 100644 index 0000000000..e209d8bd7b --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionAssessmentDaoTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class RubricCriterionAssessmentDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + private lateinit var courseDao: CourseDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var assignmentDao: AssignmentDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + rubricCriterionAssessmentDao = db.rubricCriterionAssessmentDao() + courseDao = db.courseDao() + assignmentGroupDao = db.assignmentGroupDao() + assignmentDao = db.assignmentDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + assignmentDao.insert(AssignmentEntity(Assignment(2L, assignmentGroupId = 1L), null, null, null, null)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByAssignmentId() = runTest { + val entities = listOf( + RubricCriterionAssessmentEntity(RubricCriterionAssessment(ratingId = "1"), "1", 1), + RubricCriterionAssessmentEntity(RubricCriterionAssessment(ratingId = "2"), "2", 2), + ) + rubricCriterionAssessmentDao.insertAll(entities) + + val result = rubricCriterionAssessmentDao.findByAssignmentId(1) + + Assert.assertEquals(entities.filter { it.assignmentId == 1L }, result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentForeignKey() = runTest { + rubricCriterionAssessmentDao.insert(RubricCriterionAssessmentEntity(RubricCriterionAssessment(ratingId = "1"), "1", 3)) + } + + @Test + fun testAssignmentCascade() = runTest { + rubricCriterionAssessmentDao.insert(RubricCriterionAssessmentEntity(RubricCriterionAssessment(ratingId = "1"), "1", 2)) + + assignmentDao.delete(AssignmentEntity(Assignment(2L, assignmentGroupId = 1L), null, null, null, null)) + + val result = rubricCriterionAssessmentDao.findByAssignmentId(2L) + + Assert.assertTrue(result.isEmpty()) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionDaoTest.kt new file mode 100644 index 0000000000..4ab0229e46 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionDaoTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class RubricCriterionDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var rubricCriterionDao: RubricCriterionDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var courseDao: CourseDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + rubricCriterionDao = db.rubricCriterionDao() + assignmentDao = db.assignmentDao() + courseDao = db.courseDao() + assignmentGroupDao = db.assignmentGroupDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + val rubricCriterionEntity = RubricCriterionEntity(RubricCriterion("1"), 1L) + val rubricCriterionEntity2 = RubricCriterionEntity(RubricCriterion("2"), 1L) + rubricCriterionDao.insert(rubricCriterionEntity) + rubricCriterionDao.insert(rubricCriterionEntity2) + + val result = rubricCriterionDao.findById("1") + + Assert.assertEquals(rubricCriterionEntity, result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentForeignKey() = runTest { + rubricCriterionDao.insert(RubricCriterionEntity(RubricCriterion("1"), 2L)) + } + + @Test + fun testAssignmentCascade() = runTest { + rubricCriterionDao.insert(RubricCriterionEntity(RubricCriterion("1"), 1L)) + + assignmentDao.delete(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + + val result = rubricCriterionDao.findById("1") + + Assert.assertNull(result) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionRatingDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionRatingDaoTest.kt new file mode 100644 index 0000000000..3a10aea126 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/RubricCriterionRatingDaoTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class RubricCriterionRatingDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var rubricCriterionRatingDao: RubricCriterionRatingDao + private lateinit var rubricCriterionDao: RubricCriterionDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var courseDao: CourseDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + rubricCriterionRatingDao = db.rubricCriterionRatingDao() + rubricCriterionDao = db.rubricCriterionDao() + assignmentDao = db.assignmentDao() + courseDao = db.courseDao() + assignmentGroupDao = db.assignmentGroupDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1), null, null, null, null)) + rubricCriterionDao.insert(RubricCriterionEntity(RubricCriterion("1"), 1L)) + rubricCriterionDao.insert(RubricCriterionEntity(RubricCriterion("2"), 1L)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindEntityByCourseId() = runTest { + val entities = listOf( + RubricCriterionRatingEntity(RubricCriterionRating("1"), "1"), + RubricCriterionRatingEntity(RubricCriterionRating("2"), "2"), + ) + rubricCriterionRatingDao.insertAll(entities) + + val result = rubricCriterionRatingDao.findByRubricCriterionId("1") + + Assert.assertEquals(entities.filter { it.rubricCriterionId == "1" }, result) + } + + @Test + fun testRubricCriterionCascade() = runTest { + val rubricCriterionRatingEntity = RubricCriterionRatingEntity(RubricCriterionRating("1"), "1") + + rubricCriterionRatingDao.insert(rubricCriterionRatingEntity) + + rubricCriterionDao.delete(RubricCriterionEntity(RubricCriterion("1"), 1L)) + + val result = rubricCriterionRatingDao.findByRubricCriterionId("1") + + assert(result.isEmpty()) + } + + @Test(expected = SQLiteConstraintException::class) + fun testRubricCriterionForeignKey() = runTest { + val rubricCriterionRatingEntity = RubricCriterionRatingEntity(RubricCriterionRating("1"), "3") + + rubricCriterionRatingDao.insert(rubricCriterionRatingEntity) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ScheduleItemAssignmentOverrideDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ScheduleItemAssignmentOverrideDaoTest.kt new file mode 100644 index 0000000000..d80ad58747 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ScheduleItemAssignmentOverrideDaoTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ScheduleItemAssignmentOverrideDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var scheduleItemDao: ScheduleItemDao + private lateinit var assignmentOverrideDao: AssignmentOverrideDao + private lateinit var scheduleItemAssignmentOverrideDao: ScheduleItemAssignmentOverrideDao + + @Before + fun setUp() = runTest { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + scheduleItemDao = db.scheduleItemDao() + assignmentOverrideDao = db.assignmentOverrideDao() + scheduleItemAssignmentOverrideDao = db.scheduleItemAssignmentOverrideDao() + db.courseDao().insert(CourseEntity(Course(id = 1L))) + db.assignmentGroupDao().insert(AssignmentGroupEntity(AssignmentGroup(id = 1L), 1L)) + db.assignmentDao().insert( + AssignmentEntity( + Assignment(id = 1L, courseId = 1L, assignmentGroupId = 1L), + null, + null, + null, + null + ) + ) + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindByScheduleItemId() = runTest { + val assignmentOverrideEntity = AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L)) + assignmentOverrideDao.insert(assignmentOverrideEntity) + + val scheduleItemEntity = ScheduleItemEntity(ScheduleItem(itemId = "event1"), 1L) + scheduleItemDao.insert(scheduleItemEntity) + + val expected = ScheduleItemAssignmentOverrideEntity(1L, "event1") + scheduleItemAssignmentOverrideDao.insert(expected) + + val result = scheduleItemAssignmentOverrideDao.findByScheduleItemId("event1") + + assertEquals(listOf(expected), result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testScheduleItemForeignKey() = runTest { + val assignmentOverrideEntity = AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L)) + assignmentOverrideDao.insert(assignmentOverrideEntity) + + val expected = ScheduleItemAssignmentOverrideEntity(1L, "event1") + scheduleItemAssignmentOverrideDao.insert(expected) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentOverrideForeignKey() = runTest { + val scheduleItemEntity = ScheduleItemEntity(ScheduleItem(itemId = "event1"), 1L) + scheduleItemDao.insert(scheduleItemEntity) + + val expected = ScheduleItemAssignmentOverrideEntity(1L, "event1") + scheduleItemAssignmentOverrideDao.insert(expected) + } + + @Test + fun testAssignmentOverrideCascade() = runTest { + val assignmentOverrideEntity = AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L)) + assignmentOverrideDao.insert(assignmentOverrideEntity) + + val scheduleItemEntity = ScheduleItemEntity(ScheduleItem(itemId = "event1"), 1L) + scheduleItemDao.insert(scheduleItemEntity) + + val expected = ScheduleItemAssignmentOverrideEntity(1L, "event1") + scheduleItemAssignmentOverrideDao.insert(expected) + + assignmentOverrideDao.delete(assignmentOverrideEntity) + + val result = scheduleItemAssignmentOverrideDao.findByScheduleItemId("event1") + + assert(result.isEmpty()) + } + + @Test + fun testScheduleItemCascade() = runTest { + val assignmentOverrideEntity = AssignmentOverrideEntity(AssignmentOverride(id = 1L, assignmentId = 1L)) + assignmentOverrideDao.insert(assignmentOverrideEntity) + + val scheduleItemEntity = ScheduleItemEntity(ScheduleItem(itemId = "event1"), 1L) + scheduleItemDao.insert(scheduleItemEntity) + + val expected = ScheduleItemAssignmentOverrideEntity(1L, "event1") + scheduleItemAssignmentOverrideDao.insert(expected) + + scheduleItemDao.delete(scheduleItemEntity) + + val result = scheduleItemAssignmentOverrideDao.findByScheduleItemId("event1") + + assert(result.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ScheduleItemDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ScheduleItemDaoTest.kt new file mode 100644 index 0000000000..db3a6d0386 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/ScheduleItemDaoTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.ScheduleItemEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ScheduleItemDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var scheduleItemDao: ScheduleItemDao + private lateinit var courseDao: CourseDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + scheduleItemDao = db.scheduleItemDao() + courseDao = db.courseDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertReplace() = runTest { + val scheduleItemEntity = ScheduleItemEntity(ScheduleItem(itemId = "event_1", title = "schedule item"), 1L) + val updated = scheduleItemEntity.copy(title = "updated") + + scheduleItemDao.insert(scheduleItemEntity) + scheduleItemDao.insert(updated) + + val result = scheduleItemDao.findById("event_1") + + assertEquals(updated, result) + } + + @Test + fun testFindById() = runTest { + val scheduleItemEntity = ScheduleItemEntity(ScheduleItem(itemId = "event_1", title = "schedule item"), 1L) + val scheduleItemEntity2 = ScheduleItemEntity(ScheduleItem(itemId = "event_2", title = "schedule item"), 1L) + + scheduleItemDao.insert(scheduleItemEntity) + scheduleItemDao.insert(scheduleItemEntity2) + + val result = scheduleItemDao.findById("event_2") + + assertEquals(scheduleItemEntity2, result) + } + + @Test + fun testFindByItemType() = runTest { + val assignmentEvent = ScheduleItemEntity( + ScheduleItem(itemId = "event_1", title = "schedule item", type = "assignment", contextCode = "course_1"), 1L + ) + val assignmentEvent2 = ScheduleItemEntity( + ScheduleItem(itemId = "event_2", title = "schedule item", type = "assignment", contextCode = "course_1"), 1L + ) + val calendarEvent = ScheduleItemEntity( + ScheduleItem(itemId = "event_3", title = "schedule item", type = "calendar", contextCode = "course_1"), 1L + ) + + scheduleItemDao.insert(assignmentEvent) + scheduleItemDao.insert(assignmentEvent2) + scheduleItemDao.insert(calendarEvent) + + val result = scheduleItemDao.findByItemType(listOf("course_1"), "assignment") + + assertEquals(listOf(assignmentEvent, assignmentEvent2), result) + } + + @Test + fun testDeleteAllByCourseId() = runTest { + val assignmentEvent = ScheduleItemEntity( + ScheduleItem(itemId = "event_1", title = "schedule item", type = "assignment", contextCode = "course_1"), 1L + ) + + scheduleItemDao.insert(assignmentEvent) + + val result = scheduleItemDao.findById("event_1") + + assertEquals(assignmentEvent, result) + + scheduleItemDao.deleteAllByCourseId(1L) + + val deletedResult = scheduleItemDao.findById("event_1") + + Assert.assertNull(deletedResult) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubmissionCommentDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubmissionCommentDaoTest.kt new file mode 100644 index 0000000000..a3b5c530d4 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubmissionCommentDaoTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class SubmissionCommentDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var submissionCommentDao: SubmissionCommentDao + + private lateinit var attachmentDao: AttachmentDao + private lateinit var mediaCommentDao: MediaCommentDao + private lateinit var authorDao: AuthorDao + private lateinit var courseDao: CourseDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var assignmentDao: AssignmentDao + private lateinit var submissionDao: SubmissionDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + submissionCommentDao = db.submissionCommentDao() + + attachmentDao = db.attachmentDao() + mediaCommentDao = db.mediaCommentDao() + authorDao = db.authorDao() + courseDao = db.courseDao() + assignmentGroupDao = db.assignmentGroupDao() + assignmentDao = db.assignmentDao() + submissionDao = db.submissionDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + submissionDao.insert(SubmissionEntity(Submission(1L, attempt = 1L, assignmentId = 1L), null, null)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun getSubmissionCommentWithAttachmentsById() = runTest { + // Setup all DAOs with the data needed for the query + val id = 1L + val submissionComment = SubmissionCommentEntity( + id = id, + comment = "These are the droids you are looking for", + authorId = 1, + mediaCommentId = "66", + submissionId = 1L, + attemptId = 1L + ) + + val submissionComment2 = SubmissionCommentEntity( + id = 2, + comment = "These are not the droids you are looking for", + submissionId = 1L, + attemptId = 1L + ) + + submissionCommentDao.insert(submissionComment) + submissionCommentDao.insert(submissionComment2) + authorDao.insert(AuthorEntity(id = 1, displayName = "Obi-Wan")) + attachmentDao.insert(AttachmentEntity(id = 5, submissionCommentId = 1, filename = "droids.mp4")) + mediaCommentDao.insert(MediaCommentEntity(MediaComment(mediaId = "66", displayName = "Order 66"), 1, 1)) + + // Verify correct query + val result = submissionCommentDao.findById(id) + + Assert.assertEquals(submissionComment, result!!.submissionComment) + Assert.assertEquals(1, result.attachments!!.size) + Assert.assertEquals("droids.mp4", result.attachments!!.first().filename) + Assert.assertEquals("Obi-Wan", result.author!!.displayName) + Assert.assertEquals("Order 66", result.mediaComment!!.displayName) + } + + @Test + fun testFindBySubmissionId() = runTest { + val submissionComment = SubmissionCommentEntity( + id = 1, + comment = "These are the droids you are looking for", + authorId = 1, + mediaCommentId = "66", + submissionId = 1 + ) + val submissionComment2 = SubmissionCommentEntity( + id = 2, + comment = "These are not the droids you are looking for", + submissionId = 2 + ) + submissionCommentDao.insertAll(listOf(submissionComment, submissionComment2)) + + val result = submissionCommentDao.findBySubmissionId(1) + + Assert.assertEquals(1, result.size) + Assert.assertEquals(submissionComment, result.first().submissionComment) + } + + @Test(expected = SQLiteConstraintException::class) + fun testSubmissionForeignKey() = runTest { + val submissionComment = SubmissionCommentEntity(id = 2, comment = "Comment", submissionId = 2, attemptId = 1) + + submissionCommentDao.insert(submissionComment) + } + + @Test + fun testSubmissionCascade() = runTest { + val submissionComment = SubmissionCommentEntity(id = 2, comment = "Comment", submissionId = 1, attemptId = 1) + + submissionCommentDao.insert(submissionComment) + + submissionDao.delete(SubmissionEntity(Submission(1L, attempt = 1L, assignmentId = 1L), null, null)) + + val result = submissionCommentDao.findBySubmissionId(1) + + Assert.assertTrue(result.isEmpty()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubmissionDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubmissionDaoTest.kt new file mode 100644 index 0000000000..116c7c8053 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/SubmissionDaoTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.* + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class SubmissionDaoTest { + + private lateinit var db: OfflineDatabase + private lateinit var submissionDao: SubmissionDao + private lateinit var userDao: UserDao + private lateinit var groupDao: GroupDao + private lateinit var courseDao: CourseDao + private lateinit var assignmentGroupDao: AssignmentGroupDao + private lateinit var assignmentDao: AssignmentDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + submissionDao = db.submissionDao() + userDao = db.userDao() + groupDao = db.groupDao() + courseDao = db.courseDao() + assignmentGroupDao = db.assignmentGroupDao() + assignmentDao = db.assignmentDao() + + runBlocking { + courseDao.insert(CourseEntity(Course(1L))) + assignmentGroupDao.insert(AssignmentGroupEntity(AssignmentGroup(1L), 1L)) + assignmentDao.insert(AssignmentEntity(Assignment(1L, assignmentGroupId = 1L), null, null, null, null)) + assignmentDao.insert(AssignmentEntity(Assignment(2L, assignmentGroupId = 1L), null, null, null, null)) + assignmentDao.insert(AssignmentEntity(Assignment(3L, assignmentGroupId = 1L), null, null, null, null)) + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testFindById() = runTest { + val entities = listOf( + SubmissionEntity(Submission(id = 1, body = "Body 1", attempt = 2, assignmentId = 1), null, null), + SubmissionEntity(Submission(id = 1, body = "Body 2", attempt = 1, assignmentId = 1), null, null), + SubmissionEntity(Submission(id = 2, body = "Body 3", assignmentId = 1), null, null) + ) + entities.forEach { + submissionDao.insert(it) + } + + val result = submissionDao.findById(1) + + val expected = listOf( + SubmissionEntity(Submission(id = 1, body = "Body 2", attempt = 1, assignmentId = 1), null, null), + SubmissionEntity(Submission(id = 1, body = "Body 1", attempt = 2, assignmentId = 1), null, null) + ) + + Assert.assertEquals(expected, result) + } + + @Test + fun testFindByAssignmentIds() = runTest { + val entities = listOf( + SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1), null, null), + SubmissionEntity(Submission(id = 2, body = "Body 2", assignmentId = 2), null, null), + SubmissionEntity(Submission(id = 3, body = "Body 3", assignmentId = 3), null, null) + ) + entities.forEach { + submissionDao.insert(it) + } + + val result = submissionDao.findByAssignmentIds(listOf(1, 3)) + + val expected = listOf( + SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1), null, null), + SubmissionEntity(Submission(id = 3, body = "Body 3", assignmentId = 3), null, null) + ) + + Assert.assertEquals(expected, result) + } + + @Test(expected = SQLiteConstraintException::class) + fun testGroupForeignKey() = runTest { + val submissionEntity = SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1), 1, null) + + submissionDao.insert(submissionEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testUserForeignKey() = runTest { + val submissionEntity = SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1, userId = 1), null, null) + + submissionDao.insert(submissionEntity) + } + + @Test(expected = SQLiteConstraintException::class) + fun testAssignmentForeignKey() = runTest { + val submissionEntity = SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 4, userId = 1), null, null) + + submissionDao.insert(submissionEntity) + } + + @Test + fun testGroupCascadeOnDelete() = runTest { + groupDao.insert(GroupEntity(Group(id = 1))) + + val submissionEntity = SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1), 1, null) + + submissionDao.insert(submissionEntity) + + groupDao.delete(GroupEntity(Group(id = 1))) + + val result = submissionDao.findById(1) + + Assert.assertTrue(result.isEmpty()) + } + + @Test + fun testUserSetNullOnDelete() = runTest { + userDao.insert(UserEntity(User(id = 1))) + + val submissionEntity = SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1, userId = 1), null, null) + + submissionDao.insert(submissionEntity) + + userDao.delete(UserEntity(User(id = 1))) + + val result = submissionDao.findById(1) + + Assert.assertEquals(listOf(submissionEntity.copy(userId = null)), result) + } + + @Test + fun testFindByAssignmentId() = runTest { + val entities = listOf( + SubmissionEntity(Submission(id = 1, body = "Body 1", assignmentId = 1), null, null), + SubmissionEntity(Submission(id = 2, body = "Body 2", assignmentId = 2), null, null), + ) + entities.forEach { + submissionDao.insert(it) + } + + val result = submissionDao.findByAssignmentId(1) + + Assert.assertEquals(entities.first(), result) + } +} diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/UserDaoTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/UserDaoTest.kt new file mode 100644 index 0000000000..c2032ad322 --- /dev/null +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/room/offline/daos/UserDaoTest.kt @@ -0,0 +1,78 @@ +package com.instructure.pandautils.room.offline.daos + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.entities.UserEntity +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class UserDaoTest { + private lateinit var db: OfflineDatabase + private lateinit var userDao: UserDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, OfflineDatabase::class.java).build() + userDao = db.userDao() + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testUserReplacementOnInsert() = runTest { + val newUserName = "New User 1" + val userEntity = UserEntity(User(id = 1, name = "User 1")) + val newUserEntity = UserEntity(User(id = 1, name = newUserName)) + + userDao.insert(userEntity) + userDao.insert(newUserEntity) + + val result = userDao.findById(1L) + + assertEquals(newUserName, result?.name) + } + + @Test + fun testUserFindById() = runTest { + val users = listOf( + UserEntity(User(id = 1, name = "User 1")), + UserEntity(User(id = 2, name = "User 2")), + UserEntity(User(id = 3, name = "User 3")), + ) + + users.forEach { userDao.insert(it) } + + val result = userDao.findById(2L) + + assertEquals(users[1], result) + } + + @Test + fun testUserFindByIdNoContent() = runTest { + val users = listOf( + UserEntity(User(id = 1, name = "User 1")), + UserEntity(User(id = 2, name = "User 2")), + ) + + users.forEach { userDao.insert(it) } + + val result = userDao.findById(5L) + + assertEquals(null, result) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/AndroidManifest.xml b/libs/pandautils/src/main/AndroidManifest.xml index d07ec32c6d..33566d832d 100644 --- a/libs/pandautils/src/main/AndroidManifest.xml +++ b/libs/pandautils/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + ?, useDiffUtil: Boolean = false) { + val allItems = mutableListOf() + items?.forEach { + allItems.add(it) + if (it is GroupItemViewModel && !it.collapsed) { + allItems.addAll(it.getAllVisibleItems()) + } + } + if (useDiffUtil) { - val diffResult = DiffUtil.calculateDiff(DiffUtilCallback(itemViewModels, items ?: emptyList()), false) - itemViewModels = items.orEmpty().toMutableList() + val diffResult = DiffUtil.calculateDiff(DiffUtilCallback(itemViewModels, allItems), false) + itemViewModels = allItems.toMutableList() diffResult.dispatchUpdatesTo(this) } else { - itemViewModels = items.orEmpty().toMutableList() + itemViewModels = allItems.toMutableList() notifyDataSetChanged() } @@ -81,23 +89,19 @@ open class BindableRecyclerViewAdapter : RecyclerView.Adapter()) - } } } private fun toggleGroup(group: GroupItemViewModel) { val position = itemViewModels.indexOf(group) + val items = group.getAllVisibleItems() if (group.collapsed) { - itemViewModels.removeAll(group.items) - notifyItemRangeRemoved(position + 1, group.items.size) + itemViewModels.removeAll(items) + notifyItemRangeRemoved(position + 1, items.size) } else { - itemViewModels.addAll(position + 1, group.items) + itemViewModels.addAll(position + 1, items) setupGroups(group.items.filterIsInstance()) - notifyItemRangeInserted(position + 1, group.items.size) + notifyItemRangeInserted(position + 1, items.size) } } @@ -110,7 +114,6 @@ open class BindableRecyclerViewAdapter : RecyclerView.Adapter { + emptyView.setVisible() + emptyView.setLoadingWithAnimation(state.titleRes, state.messageRes, state.animationRes) + } is ViewState.Refresh -> emptyView.setGone() is ViewState.Empty -> { emptyView.setVisible() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt index 4aea5325ab..88f2d515c9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/binding/GroupItemViewModel.kt @@ -23,12 +23,23 @@ import com.instructure.pandautils.mvvm.ItemViewModel abstract class GroupItemViewModel( val collapsable: Boolean, - @get:Bindable var collapsed: Boolean = collapsable, - val items: List + @get:Bindable open var collapsed: Boolean = collapsable, + var items: List ) : ItemViewModel, BaseObservable() { open fun toggleItems() { collapsed = !collapsed notifyPropertyChanged(BR.collapsed) } + + fun getAllVisibleItems(): List { + val result = mutableListOf() + items.forEach { + result += it + if (it is GroupItemViewModel && !it.collapsed) { + result += it.getAllVisibleItems() + } + } + return result + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt index bd947ba060..8280827d89 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/ApplicationModule.kt @@ -23,10 +23,17 @@ import android.content.res.Resources import android.webkit.CookieManager import androidx.work.WorkManager import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.canvasapi2.apis.FileFolderAPI import com.instructure.canvasapi2.managers.OAuthManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.offline.sync.HtmlParser +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao import com.instructure.pandautils.typeface.TypefaceBehavior import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.HtmlContentFormatter +import com.instructure.pandautils.utils.StorageUtils import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -91,4 +98,22 @@ class ApplicationModule { fun provideCookieManager(): CookieManager { return CookieManager.getInstance() } + + @Provides + @Singleton + fun provideStorageUtils(@ApplicationContext context: Context): StorageUtils { + return StorageUtils(context) + } + + @Provides + fun provideHtmlParses( + localFileDao: LocalFileDao, + apiPrefs: ApiPrefs, + fileFolderDao: FileFolderDao, + @ApplicationContext context: Context, + fileSyncSettingsDao: FileSyncSettingsDao, + fileFolderApi: FileFolderAPI.FilesFoldersInterface + ): HtmlParser { + return HtmlParser(localFileDao, apiPrefs, fileFolderDao, context, fileSyncSettingsDao, fileFolderApi) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/CourseFileModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/CourseFileModule.kt new file mode 100644 index 0000000000..c7acd7cabc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/CourseFileModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.di + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class CourseFileModule { + + @Provides + @Singleton + fun provideCourseFileRepository(fileFolderApi: FileFolderAPI.FilesFoldersInterface): CourseFileSharedRepository { + return CourseFileSharedRepository(fileFolderApi) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DatabaseModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DatabaseModule.kt index 3d7f4675d5..c4890d639e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DatabaseModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DatabaseModule.kt @@ -2,10 +2,6 @@ package com.instructure.pandautils.di import com.instructure.pandautils.room.appdatabase.AppDatabase import com.instructure.pandautils.room.appdatabase.daos.* -import com.instructure.pandautils.room.common.daos.AttachmentDao -import com.instructure.pandautils.room.common.daos.AuthorDao -import com.instructure.pandautils.room.common.daos.MediaCommentDao -import com.instructure.pandautils.room.common.daos.SubmissionCommentDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt index c91f02ffb1..9a91cc948d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt @@ -1,9 +1,14 @@ package com.instructure.pandautils.di -import com.instructure.canvasapi2.managers.DiscussionManager -import com.instructure.canvasapi2.managers.FeaturesManager -import com.instructure.canvasapi2.managers.GroupManager +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.FeaturesAPI +import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelper +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperLocalDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource +import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade import com.instructure.pandautils.utils.FeatureFlagProvider import dagger.Module import dagger.Provides @@ -16,11 +21,26 @@ class DiscussionModule { @Provides fun provideDiscussionRouteHelper( - featuresManager: FeaturesManager, - featureFlagProvider: FeatureFlagProvider, - discussionManager: DiscussionManager, - groupManager: GroupManager + discussionRouteHelperRepository: DiscussionRouteHelperRepository ): DiscussionRouteHelper { - return DiscussionRouteHelper(featuresManager, featureFlagProvider, discussionManager, groupManager) + return DiscussionRouteHelper(discussionRouteHelperRepository) + } + + @Provides + fun provideDiscussionRouteHelperNetworkDataSource( + discussionApi: DiscussionAPI.DiscussionInterface, + groupApi: GroupAPI.GroupInterface, + featuresApi: FeaturesAPI.FeaturesInterface, + featureFlagProvider: FeatureFlagProvider + ): DiscussionRouteHelperNetworkDataSource { + return DiscussionRouteHelperNetworkDataSource(discussionApi, groupApi, featuresApi, featureFlagProvider) + } + + @Provides + fun provideDiscussionRouteHelperLocalDataSource( + discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + groupFacade: GroupFacade + ): DiscussionRouteHelperLocalDataSource { + return DiscussionRouteHelperLocalDataSource(discussionTopicHeaderFacade, groupFacade) } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt index 7199dc3a53..5a38909b2a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/FeatureFlagModule.kt @@ -25,12 +25,14 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class FeatureFlagModule { @Provides + @Singleton fun provideFeatureFlagProvider(userManager: UserManager, apiPrefs: ApiPrefs, featuresApi: FeaturesAPI.FeaturesInterface, environmentFeatureFlagsDao: EnvironmentFeatureFlagsDao): FeatureFlagProvider { return FeatureFlagProvider(userManager, apiPrefs, featuresApi, environmentFeatureFlagsDao) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/NetworkStateProviderModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/NetworkStateProviderModule.kt new file mode 100644 index 0000000000..709fce9b62 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/NetworkStateProviderModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.di + +import android.content.Context +import com.instructure.pandautils.room.offline.daos.* +import com.instructure.pandautils.room.offline.facade.* +import com.instructure.pandautils.utils.NetworkStateProvider +import com.instructure.pandautils.utils.NetworkStateProviderImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class NetworkStateProviderModule { + + @Provides + @Singleton + fun provideNetworkStateProvider(@ApplicationContext context: Context): NetworkStateProvider { + return NetworkStateProviderImpl(context) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineContentModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineContentModule.kt new file mode 100644 index 0000000000..2e7afc60f4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineContentModule.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.di + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository +import com.instructure.pandautils.features.offline.offlinecontent.OfflineContentRepository +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class OfflineContentModule { + + @Provides + fun provideOfflineContentRepository( + coursesApi: CourseAPI.CoursesInterface, + courseSyncSettingsDao: CourseSyncSettingsDao, + fileSyncSettingsDao: FileSyncSettingsDao, + courseFileSharedRepository: CourseFileSharedRepository, + syncSettingsFacade: SyncSettingsFacade, + localFileDao: LocalFileDao, + fileSyncProgressDao: FileSyncProgressDao + ): OfflineContentRepository { + return OfflineContentRepository( + coursesApi, + courseSyncSettingsDao, + fileSyncSettingsDao, + courseFileSharedRepository, + syncSettingsFacade, + localFileDao, + fileSyncProgressDao + ) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt new file mode 100644 index 0000000000..fea3f2b3d2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineDatabaseProviderModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.di + +import android.content.Context +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.room.offline.OfflineDatabaseProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class OfflineDatabaseProviderModule { + + @Provides + @Singleton + fun provideOfflineDatabaseProvider(@ApplicationContext context: Context): DatabaseProvider { + return OfflineDatabaseProvider(context) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt new file mode 100644 index 0000000000..90881412a1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineModule.kt @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.di + +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.room.offline.DatabaseProvider +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.* +import com.instructure.pandautils.room.offline.facade.* +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class OfflineModule { + + @Provides + fun provideOfflineDatabase(offlineDatabaseProvider: DatabaseProvider, apiPrefs: ApiPrefs): OfflineDatabase { + val userId = if (apiPrefs.isMasquerading || apiPrefs.isMasqueradingFromQRCode) apiPrefs.masqueradeId else apiPrefs.user?.id + return offlineDatabaseProvider.getDatabase(userId) + } + + @Provides + fun provideCourseDao(appDatabase: OfflineDatabase): CourseDao { + return appDatabase.courseDao() + } + + @Provides + fun provideEnrollmentDao(appDatabase: OfflineDatabase): EnrollmentDao { + return appDatabase.enrollmentDao() + } + + @Provides + fun provideGradesDao(appDatabase: OfflineDatabase): GradesDao { + return appDatabase.gradesDao() + } + + @Provides + fun provideGradingPeriodDao(appDatabase: OfflineDatabase): GradingPeriodDao { + return appDatabase.gradingPeriodDao() + } + + @Provides + fun provideSectionDao(appDatabase: OfflineDatabase): SectionDao { + return appDatabase.sectionDao() + } + + @Provides + fun provideTermDao(appDatabase: OfflineDatabase): TermDao { + return appDatabase.termDao() + } + + @Provides + fun provideUserCalendarDao(appDatabase: OfflineDatabase): UserCalendarDao { + return appDatabase.userCalendarDao() + } + + @Provides + fun provideUserDao(appDatabase: OfflineDatabase): UserDao { + return appDatabase.userDao() + } + + @Provides + fun provideCourseGradingPeriodDao(appDatabase: OfflineDatabase): CourseGradingPeriodDao { + return appDatabase.courseGradingPeriodDao() + } + + @Provides + fun provideTabDao(appDatabase: OfflineDatabase): TabDao { + return appDatabase.tabDao() + } + + @Provides + fun provideCourseSyncSettingsDao(appDatabase: OfflineDatabase): CourseSyncSettingsDao { + return appDatabase.courseSyncSettingsDao() + } + + @Provides + fun providePageDao(appDatabase: OfflineDatabase): PageDao { + return appDatabase.pageDao() + } + + @Provides + fun provideAssignmentGroupDao(appDatabase: OfflineDatabase): AssignmentGroupDao { + return appDatabase.assignmentGroupDao() + } + + @Provides + fun provideAssignmentDao(appDatabase: OfflineDatabase): AssignmentDao { + return appDatabase.assignmentDao() + } + + @Provides + fun provideRubricSettings(appDatabase: OfflineDatabase): RubricSettingsDao { + return appDatabase.rubricSettingsDao() + } + + @Provides + fun provideSubmissionDao(appDatabase: OfflineDatabase): SubmissionDao { + return appDatabase.submissionDao() + } + + @Provides + fun provideGroupDao(appDatabase: OfflineDatabase): GroupDao { + return appDatabase.groupDao() + } + + @Provides + fun providePlannerOverrideDao(appDatabase: OfflineDatabase): PlannerOverrideDao { + return appDatabase.plannerOverrideDao() + } + + @Provides + fun provideDiscussionTopicHeaderDao(appDatabase: OfflineDatabase): DiscussionTopicHeaderDao { + return appDatabase.discussionTopicHeaderDao() + } + + @Provides + fun provideDiscussionParticipantDao(appDatabase: OfflineDatabase): DiscussionParticipantDao { + return appDatabase.discussionParticipantDao() + } + + @Provides + fun provideAssignmentScoreStatisticsDao(appDatabase: OfflineDatabase): AssignmentScoreStatisticsDao { + return appDatabase.assignmentScoreStatisticsDao() + } + + @Provides + fun provideRubricCriterionDao(appDatabase: OfflineDatabase): RubricCriterionDao { + return appDatabase.rubricCriterionDao() + } + + @Provides + fun provideQuizDao(appDatabase: OfflineDatabase): QuizDao { + return appDatabase.quizDao() + } + + @Provides + fun provideLockInfoDao(appDatabase: OfflineDatabase): LockInfoDao { + return appDatabase.lockInfoDao() + } + + @Provides + fun provideLockedModuleDao(appDatabase: OfflineDatabase): LockedModuleDao { + return appDatabase.lockedModuleDao() + } + + @Provides + fun provideModuleNameDao(appDatabase: OfflineDatabase): ModuleNameDao { + return appDatabase.moduleNameDao() + } + + @Provides + fun provideModuleCompletionRequirementDao(appDatabase: OfflineDatabase): ModuleCompletionRequirementDao { + return appDatabase.moduleCompletionRequirementDao() + } + + @Provides + fun provideDashboardCardDao(offlineDatabase: OfflineDatabase): DashboardCardDao { + return offlineDatabase.dashboardCardDao() + } + + @Provides + fun provideCourseSettingsDao(offlineDatabase: OfflineDatabase): CourseSettingsDao { + return offlineDatabase.courseSettingsDao() + } + + @Provides + fun provideScheduleItemDao(offlineDatabase: OfflineDatabase): ScheduleItemDao { + return offlineDatabase.scheduleItemDao() + } + + @Provides + fun provideScheduleItemAssignmentOverrideDao(offlineDatabase: OfflineDatabase): ScheduleItemAssignmentOverrideDao { + return offlineDatabase.scheduleItemAssignmentOverrideDao() + } + + @Provides + fun provideAssignmentOverrideDao(offlineDatabase: OfflineDatabase): AssignmentOverrideDao { + return offlineDatabase.assignmentOverrideDao() + } + + @Provides + fun provideModuleObjectDao(offlineDatabase: OfflineDatabase): ModuleObjectDao { + return offlineDatabase.moduleObjectDao() + } + + @Provides + fun provideModuleItemDao(offlineDatabase: OfflineDatabase): ModuleItemDao { + return offlineDatabase.moduleItemDao() + } + + @Provides + fun provideModuleContentDetailsDao(offlineDatabase: OfflineDatabase): ModuleContentDetailsDao { + return offlineDatabase.moduleContentDetailsDao() + } + + @Provides + fun provideMasteryPathDao(offlineDatabase: OfflineDatabase): MasteryPathDao { + return offlineDatabase.masteryPathDao() + } + + @Provides + fun provideAssignmentSetDao(offlineDatabase: OfflineDatabase): AssignmentSetDao { + return offlineDatabase.assignmentSetDao() + } + + @Provides + fun provideMasteryPathAssignmentDao(offlineDatabase: OfflineDatabase): MasteryPathAssignmentDao { + return offlineDatabase.masteryPathAssignmentDao() + } + + @Provides + fun provideAssignmentFacade( + assignmentGroupDao: AssignmentGroupDao, + assignmentDao: AssignmentDao, + plannerOverrideDao: PlannerOverrideDao, + rubricSettingsDao: RubricSettingsDao, + submissionFacade: SubmissionFacade, + discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + assignmentScoreStatisticsDao: AssignmentScoreStatisticsDao, + rubricCriterionDao: RubricCriterionDao, + lockInfoFacade: LockInfoFacade, + rubricCriterionRatingDao: RubricCriterionRatingDao, + assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + offlineDatabase: OfflineDatabase + ): AssignmentFacade { + return AssignmentFacade( + assignmentGroupDao, + assignmentDao, + plannerOverrideDao, + rubricSettingsDao, + submissionFacade, + discussionTopicHeaderFacade, + assignmentScoreStatisticsDao, + rubricCriterionDao, + lockInfoFacade, + rubricCriterionRatingDao, + assignmentRubricCriterionDao, + offlineDatabase + ) + } + + @Provides + fun provideSubmissionFacade( + submissionDao: SubmissionDao, + groupDao: GroupDao, + mediaCommentDao: MediaCommentDao, + userDao: UserDao, + submissionCommentDao: SubmissionCommentDao, + attachmentDao: AttachmentDao, + authorDao: AuthorDao, + rubricCriterionAssessmentDao: RubricCriterionAssessmentDao + ): SubmissionFacade { + return SubmissionFacade( + submissionDao, groupDao, mediaCommentDao, userDao, + submissionCommentDao, attachmentDao, authorDao, rubricCriterionAssessmentDao + ) + } + + @Provides + fun provideDiscussionTopicHeaderFacade( + discussionTopicHeaderDao: DiscussionTopicHeaderDao, + discussionParticipantDao: DiscussionParticipantDao, + discussionTopicPermissionDao: DiscussionTopicPermissionDao, + offlineDatabase: OfflineDatabase, + ): DiscussionTopicHeaderFacade { + return DiscussionTopicHeaderFacade(discussionTopicHeaderDao, discussionParticipantDao, discussionTopicPermissionDao, offlineDatabase) + } + + @Provides + fun provideCourseFacade( + termDao: TermDao, + courseDao: CourseDao, + gradingPeriodDao: GradingPeriodDao, + courseGradingPeriodDao: CourseGradingPeriodDao, + sectionDao: SectionDao, + tabDao: TabDao, + enrollmentFacade: EnrollmentFacade, + courseSettingsDao: CourseSettingsDao + ): CourseFacade { + return CourseFacade( + termDao, + courseDao, + gradingPeriodDao, + courseGradingPeriodDao, + sectionDao, + tabDao, + enrollmentFacade, + courseSettingsDao + ) + } + + @Provides + fun provideEnrollmentFacade( + userDao: UserDao, + enrollmentDao: EnrollmentDao, + gradesDao: GradesDao, + ): EnrollmentFacade { + return EnrollmentFacade(userDao, enrollmentDao, gradesDao) + } + + @Provides + fun provideSyncSettingsDao(appDatabase: OfflineDatabase): SyncSettingsDao { + return appDatabase.syncSettingsDao() + } + + @Provides + fun provideSyncSettingsFacade(syncSettingsDao: SyncSettingsDao): SyncSettingsFacade { + return SyncSettingsFacade(syncSettingsDao) + } + + @Provides + fun provideLockInfoFacade( + lockInfoDao: LockInfoDao, + lockedModuleDao: LockedModuleDao, + moduleNameDao: ModuleNameDao, + completionRequirementDao: ModuleCompletionRequirementDao + ): LockInfoFacade { + return LockInfoFacade(lockInfoDao, lockedModuleDao, moduleNameDao, completionRequirementDao) + } + + @Provides + fun provideFileSyncSettingsDao(appDatabase: OfflineDatabase): FileSyncSettingsDao { + return appDatabase.fileSyncSettingsDao() + } + + @Provides + fun provideScheduleItemFacade( + scheduleItemDao: ScheduleItemDao, + assignmentDao: AssignmentDao, + assignmentOverrideDao: AssignmentOverrideDao, + scheduleItemAssignmentOverrideDao: ScheduleItemAssignmentOverrideDao, + offlineDatabase: OfflineDatabase + ): ScheduleItemFacade { + return ScheduleItemFacade(scheduleItemDao, assignmentOverrideDao, scheduleItemAssignmentOverrideDao, assignmentDao, offlineDatabase) + } + + @Provides + fun provideConferenceDao(appDatabase: OfflineDatabase): ConferenceDao { + return appDatabase.conferenceDao() + } + + @Provides + fun provideConferenceRecodingDao(appDatabase: OfflineDatabase): ConferenceRecodingDao { + return appDatabase.conferenceRecordingDao() + } + + @Provides + fun provideConferenceFacade( + conferenceDao: ConferenceDao, + conferenceRecodingDao: ConferenceRecodingDao, + offlineDatabase: OfflineDatabase + ): ConferenceFacade { + return ConferenceFacade(conferenceDao, conferenceRecodingDao, offlineDatabase) + } + + @Provides + fun providePeopleFacade( + userDao: UserDao, + enrollmentDao: EnrollmentDao, + sectionDao: SectionDao, + enrollmentFacade: EnrollmentFacade, + offlineDatabase: OfflineDatabase + ): UserFacade { + return UserFacade(userDao, enrollmentDao, sectionDao, enrollmentFacade, offlineDatabase) + } + + @Provides + fun provideModuleFacade( + moduleObjectDao: ModuleObjectDao, + moduleItemDao: ModuleItemDao, + completionRequirementDao: ModuleCompletionRequirementDao, + moduleContentDetailsDao: ModuleContentDetailsDao, + lockInfoFacade: LockInfoFacade, + masteryPathFacade: MasteryPathFacade, + offlineDatabase: OfflineDatabase + ): ModuleFacade { + return ModuleFacade( + moduleObjectDao, + moduleItemDao, + completionRequirementDao, + moduleContentDetailsDao, + lockInfoFacade, + masteryPathFacade, + offlineDatabase + ) + } + + @Provides + fun provideMasteryPathFacade( + masteryPathDao: MasteryPathDao, + assignmentSetDao: AssignmentSetDao, + masteryPathAssignmentDao: MasteryPathAssignmentDao, + assignmentFacade: AssignmentFacade + ): MasteryPathFacade { + return MasteryPathFacade(masteryPathDao, masteryPathAssignmentDao, assignmentSetDao, assignmentFacade) + } + + @Provides + fun provideCourseFeaturesDao(appDatabase: OfflineDatabase): CourseFeaturesDao { + return appDatabase.courseFeaturesDao() + } + + @Provides + fun provideAttachmentDao(offlineDatabase: OfflineDatabase): AttachmentDao { + return offlineDatabase.attachmentDao() + } + + @Provides + fun provideAuthorDao(offlineDatabase: OfflineDatabase): AuthorDao { + return offlineDatabase.authorDao() + } + + @Provides + fun provideMediaCommentDao(offlineDatabase: OfflineDatabase): MediaCommentDao { + return offlineDatabase.mediaCommentDao() + } + + @Provides + fun provideSubmissionCommentDao(offlineDatabase: OfflineDatabase): SubmissionCommentDao { + return offlineDatabase.submissionCommentDao() + } + + @Provides + fun provideRubricCriterionAssessmentDao(offlineDatabase: OfflineDatabase): RubricCriterionAssessmentDao { + return offlineDatabase.rubricCriterionAssessmentDao() + } + + @Provides + fun provideRubricCriterionRatingDao(offlineDatabase: OfflineDatabase): RubricCriterionRatingDao { + return offlineDatabase.rubricCriterionRatingDao() + } + + @Provides + fun provideAssignmentRubricCriterionDao(offlineDatabase: OfflineDatabase): AssignmentRubricCriterionDao { + return offlineDatabase.assignmentRubricCriterionDao() + } + + @Provides + fun providePageFacade(pageDao: PageDao, lockInfoFacade: LockInfoFacade, offlineDatabase: OfflineDatabase): PageFacade { + return PageFacade(pageDao, lockInfoFacade, offlineDatabase) + } + + @Provides + fun provideFileFolderDao(appDatabase: OfflineDatabase): FileFolderDao { + return appDatabase.fileFolderDao() + } + + @Provides + fun provideLocalFileDao(appDatabase: OfflineDatabase): LocalFileDao { + return appDatabase.localFileDao() + } + + @Provides + fun provideEditDashboardItemDao(appDatabase: OfflineDatabase): EditDashboardItemDao { + return appDatabase.editDashboardItemDao() + } + + @Provides + fun provideCourseProgressDao(appDatabase: OfflineDatabase): CourseSyncProgressDao { + return appDatabase.courseSyncProgressDao() + } + + @Provides + fun provideFileSyncProgressDao(appDatabase: OfflineDatabase): FileSyncProgressDao { + return appDatabase.fileSyncProgressDao() + } + + @Provides + fun provideDiscussionEntryDao(appDatabase: OfflineDatabase): DiscussionEntryDao { + return appDatabase.discussionEntryDao() + } + + @Provides + fun provideDiscussionTopicDao(appDatabase: OfflineDatabase): DiscussionTopicDao { + return appDatabase.discussionTopicDao() + } + + @Provides + fun provideDiscussionTopicPermissionDao(appDatabase: OfflineDatabase): DiscussionTopicPermissionDao { + return appDatabase.discussionTopicPermissionDao() + } + + @Provides + fun provideGroupUserDao(appDatabase: OfflineDatabase): GroupUserDao { + return appDatabase.groupUserDao() + } + + @Provides + fun provideDiscussionTopicFacade( + discussionEntryDao: DiscussionEntryDao, + discussionParticipantDao: DiscussionParticipantDao, + discussionTopicDao: DiscussionTopicDao, + ): DiscussionTopicFacade { + return DiscussionTopicFacade(discussionTopicDao, discussionParticipantDao, discussionEntryDao) + } + + @Provides + fun provideGroupFacade( + groupUserDao: GroupUserDao, + groupDao: GroupDao, + userDao: UserDao, + ): GroupFacade { + return GroupFacade(groupUserDao, groupDao, userDao) + } + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt new file mode 100644 index 0000000000..d0a0af9756 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/OfflineSyncModule.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.di + +import android.content.Context +import androidx.work.WorkManager +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver +import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext + +@Module +@InstallIn(ViewModelComponent::class) +class OfflineSyncModule { + + @Provides + fun provideOfflineSyncHelper( + workManager: WorkManager, + syncSettingsFacade: SyncSettingsFacade, + apiPrefs: ApiPrefs + ): OfflineSyncHelper { + return OfflineSyncHelper(workManager, syncSettingsFacade, apiPrefs) + } + + @Provides + fun provideAggregateProgressObserver( + @ApplicationContext context: Context, + courseSyncProgressDao: CourseSyncProgressDao, + fileSyncProgressDao: FileSyncProgressDao + ): AggregateProgressObserver { + return AggregateProgressObserver(context, courseSyncProgressDao, fileSyncProgressDao) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/discussions/DiscussionUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/discussions/DiscussionUtils.kt index a0ef2f598c..fe22c90bf1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/discussions/DiscussionUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/discussions/DiscussionUtils.kt @@ -19,7 +19,14 @@ package com.instructure.pandautils.discussions import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect import android.net.Uri import android.util.Base64 import android.view.View @@ -32,17 +39,21 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionEntry import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.utils.DateHelper -import com.instructure.canvasapi2.utils.Logger import com.instructure.canvasapi2.utils.toDate import com.instructure.canvasapi2.utils.tryOrNull import com.instructure.pandautils.R -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.DP +import com.instructure.pandautils.utils.Placeholder +import com.instructure.pandautils.utils.ProfileUtils +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.isCourse +import com.instructure.pandautils.utils.isGroup import com.instructure.pandautils.views.CanvasWebView import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream -import java.net.URLEncoder -import java.util.* +import java.util.Locale +import java.util.UUID import java.util.regex.Pattern object DiscussionUtils { @@ -231,7 +242,7 @@ object DiscussionUtils { /** * This function should only be called from a background thread as it can be very expensive to execute */ - fun createDiscussionTopicHtml( + suspend fun createDiscussionTopicHtml( context: Context, isTablet: Boolean, canvasContext: CanvasContext, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/DashboardCourseItem.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/DashboardCourseItem.kt new file mode 100644 index 0000000000..8fc5d01c00 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/DashboardCourseItem.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.dashboard + +import com.instructure.canvasapi2.models.Course + +data class DashboardCourseItem(val course: Course, val availableOffline: Boolean, val available: Boolean) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardFragment.kt index 9bef9fb458..8b11c4cd41 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardFragment.kt @@ -76,7 +76,7 @@ class EditDashboardFragment : Fragment() { } private fun setupToolbar() { - binding.toolbar.setTitle(R.string.editDashboard) + binding.toolbar.setTitle(R.string.allCoursesScreenHeader) binding.toolbar.setupAsBackButton(this) binding.toolbar.addSearch { viewModel.queryItems(it) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardRepository.kt index 876bab291c..3f57738a58 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardRepository.kt @@ -21,8 +21,11 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group interface EditDashboardRepository { - suspend fun getCurses(): List> + suspend fun getCourses(): List> suspend fun getGroups(): List fun isOpenable(course: Course): Boolean fun isFavoriteable(course: Course): Boolean + + suspend fun getSyncedCourseIds(): Set + suspend fun offlineEnabled(): Boolean } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewData.kt index 38f8edad9c..88326ac64d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewData.kt @@ -39,5 +39,6 @@ enum class EditDashboardItemViewType(val viewType: Int) { GROUP(1), HEADER(2), DESCRIPTION(3), - ENROLLMENT(4) + ENROLLMENT(4), + NOTE(5), } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewModel.kt index 4ac2453e43..9ee7c2dc59 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/EditDashboardViewModel.kt @@ -27,10 +27,16 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.Logger import com.instructure.pandautils.R -import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.* +import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.EditDashboardCourseItemViewModel +import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.EditDashboardDescriptionItemViewModel +import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.EditDashboardEnrollmentItemViewModel +import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.EditDashboardGroupItemViewModel +import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.EditDashboardHeaderViewModel +import com.instructure.pandautils.features.dashboard.edit.itemviewmodels.EditDashboardNoteItemViewModel import com.instructure.pandautils.mvvm.Event import com.instructure.pandautils.mvvm.ItemViewModel import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.NetworkStateProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,7 +45,8 @@ import javax.inject.Inject class EditDashboardViewModel @Inject constructor( private val courseManager: CourseManager, private val groupManager: GroupManager, - private val repository: EditDashboardRepository + private val repository: EditDashboardRepository, + private val networkStateProvider: NetworkStateProvider ) : ViewModel() { val state: LiveData @@ -67,12 +74,17 @@ class EditDashboardViewModel @Inject constructor( private lateinit var groupHeader: EditDashboardHeaderViewModel private lateinit var courseHeader: EditDashboardHeaderViewModel + private var noteItem: EditDashboardNoteItemViewModel? = null + private var noteItemHidden = false private lateinit var currentCoursesViewData: List private lateinit var pastCoursesViewData: List private lateinit var futureCoursesViewData: List private lateinit var groupsViewData: List + private var syncedCourseIds = emptySet() + private var offlineEnabled = false + var hasChanges = false init { @@ -83,11 +95,14 @@ class EditDashboardViewModel @Inject constructor( fun loadItems() { viewModelScope.launch { try { - val courses = repository.getCurses() + val courses = repository.getCourses() currentCourses = courses.getOrNull(0).orEmpty() pastCourses = courses.getOrNull(1).orEmpty() futureCourses = courses.getOrNull(2).orEmpty() + syncedCourseIds = repository.getSyncedCourseIds() + offlineEnabled = repository.offlineEnabled() + courseMap = (currentCourses + pastCourses + futureCourses).associateBy { it.id } groups = repository.getGroups() @@ -97,7 +112,7 @@ class EditDashboardViewModel @Inject constructor( val items = createListItems(currentCourses, pastCourses, futureCourses, groups) _data.postValue(EditDashboardViewData(items)) if (items.isEmpty()) { - _state.postValue(ViewState.Empty(R.string.edit_dashboard_empty_title, R.string.edit_dashboard_empty_message, R.drawable.ic_panda_nocourses)) + postEmptyState() } else { _state.postValue(ViewState.Success) } @@ -109,6 +124,15 @@ class EditDashboardViewModel @Inject constructor( } } + private fun postEmptyState() { + val offlineMode = offlineEnabled && !networkStateProvider.isOnline() + if (offlineMode) { + _state.postValue(ViewState.Empty(R.string.editDashboardOfflineMode, R.string.editDashboardOfflineModeMessage, R.drawable.ic_panda_nocourses)) + } else { + _state.postValue(ViewState.Empty(R.string.edit_dashboard_empty_title,R.string.edit_dashboard_empty_message, R.drawable.ic_panda_nocourses)) + } + } + private fun handleAction(action: EditDashboardItemAction) { when (action) { is EditDashboardItemAction.OpenGroup -> { @@ -283,7 +307,7 @@ class EditDashboardViewModel @Inject constructor( } private fun selectAllCourses() { - val coursesToFavorite = (currentCoursesViewData + futureCoursesViewData).filter { !it.isFavorite && it.favoriteable } + val coursesToFavorite = (currentCoursesViewData + futureCoursesViewData).filter { !it.isFavorite && it.favoritableOnline } var counter = 0 coursesToFavorite.forEach { viewModelScope.launch { @@ -327,46 +351,36 @@ class EditDashboardViewModel @Inject constructor( private fun getCurrentCourses(courses: List): List { favoriteCourseMap.clear() favoriteCourseMap.putAll(courses.filter { it.isFavorite }.associateBy { it.id }) - return courses.map { - EditDashboardCourseItemViewModel( - id = it.id, - name = it.name, - isFavorite = it.isFavorite, - favoriteable = repository.isFavoriteable(it), - openable = repository.isOpenable(it), - termTitle = "${it.term?.name} | ${it.enrollments?.get(0)?.type?.apiTypeString}", - actionHandler = ::handleAction - ) - } + return courses.map { createCourseItem(it) } } private fun getPastCourses(courses: List): List { - return courses.map { - EditDashboardCourseItemViewModel( - id = it.id, - name = it.name, - isFavorite = it.isFavorite, - favoriteable = repository.isFavoriteable(it), - openable = repository.isOpenable(it), - termTitle = "${it.term?.name} | ${it.enrollments?.get(0)?.type?.apiTypeString}", - actionHandler = ::handleAction - ) - } + return courses.map { createCourseItem(it) } } private fun getFutureCourses(courses: List): List { favoriteCourseMap.putAll(courses.filter { it.isFavorite }.associateBy { it.id }) - return courses.map { - EditDashboardCourseItemViewModel( - id = it.id, - name = it.name, - isFavorite = it.isFavorite, - favoriteable = repository.isFavoriteable(it), - openable = repository.isOpenable(it), - termTitle = "${it.term?.name} | ${it.enrollments?.get(0)?.type?.apiTypeString}", - actionHandler = ::handleAction - ) - } + return courses.map { createCourseItem(it) } + } + + private fun createCourseItem(course: Course): EditDashboardCourseItemViewModel { + val termName = course.term?.name + val enrollmentType = course.enrollments?.firstOrNull()?.type?.apiTypeString.orEmpty() + val termTitle = if (termName != null) "$termName | $enrollmentType" else enrollmentType + val availableOffline = syncedCourseIds.contains(course.id) + + return EditDashboardCourseItemViewModel( + id = course.id, + name = course.name, + isFavorite = course.isFavorite, + favoritableOnline = repository.isFavoriteable(course), + openable = repository.isOpenable(course), + termTitle = termTitle, + online = networkStateProvider.isOnline(), + availableOffline = availableOffline, + enabled = !offlineEnabled || networkStateProvider.isOnline() || availableOffline, + actionHandler = ::handleAction + ) } private fun getGroups(groups: List): List { @@ -386,8 +400,15 @@ class EditDashboardViewModel @Inject constructor( val items = mutableListOf() if (currentCoursesViewData.isNotEmpty() || pastCoursesViewData.isNotEmpty() || futureCoursesViewData.isNotEmpty()) { - val courseHeaderTitle = if (isFiltered) R.string.courses else R.string.all_courses - courseHeader = EditDashboardHeaderViewModel(courseHeaderTitle, favoriteCourseMap.isNotEmpty(), ::selectAllCourses, ::deselectAllCourses) + createNoteItem()?.let { items.add(it) } + + courseHeader = EditDashboardHeaderViewModel( + R.string.courses, + favoriteCourseMap.isNotEmpty(), + ::selectAllCourses, + ::deselectAllCourses, + networkStateProvider.isOnline() + ) items.add(courseHeader) items.add(EditDashboardDescriptionItemViewModel(R.string.edit_dashboard_course_description)) } @@ -405,8 +426,13 @@ class EditDashboardViewModel @Inject constructor( items.addAll(futureCoursesViewData) } if (groupsViewData.isNotEmpty()) { - val groupHeaderTitle = if (isFiltered) R.string.groups else R.string.all_groups - groupHeader = EditDashboardHeaderViewModel(groupHeaderTitle, favoriteGroupMap.isNotEmpty(), ::selectAllGroups, ::deselectAllGroups) + groupHeader = EditDashboardHeaderViewModel( + R.string.groups, + favoriteGroupMap.isNotEmpty(), + ::selectAllGroups, + ::deselectAllGroups, + networkStateProvider.isOnline() + ) items.add(groupHeader) items.add(EditDashboardDescriptionItemViewModel(R.string.edit_dashboard_group_description)) items.addAll(groupsViewData) @@ -415,6 +441,18 @@ class EditDashboardViewModel @Inject constructor( return items } + private fun createNoteItem(): EditDashboardNoteItemViewModel? { + noteItem = if (!networkStateProvider.isOnline() && offlineEnabled && !noteItemHidden) { + EditDashboardNoteItemViewModel { + val newItems = _data.value?.items?.minus(noteItem)?.filterNotNull() + _data.postValue(EditDashboardViewData(newItems.orEmpty())) + noteItemHidden = true + } + } else null + + return noteItem + } + fun queryItems(query: String) { val items = if (query.isBlank()) { createListItems(currentCourses, pastCourses, futureCourses, groups) @@ -427,7 +465,7 @@ class EditDashboardViewModel @Inject constructor( createListItems(queriedCurrentCourses, queriedPastCourses, queriedFutureCourses, queriedGroups, true) } if (items.isEmpty()) { - _state.postValue(ViewState.Empty(R.string.edit_dashboard_empty_title, R.string.edit_dashboard_empty_message, R.drawable.ic_panda_nocourses)) + postEmptyState() } else { _state.postValue(ViewState.Success) } @@ -435,8 +473,12 @@ class EditDashboardViewModel @Inject constructor( } fun refresh() { - _state.postValue(ViewState.Refresh) - loadItems() + if (networkStateProvider.isOnline()) { + _state.postValue(ViewState.Refresh) + loadItems() + } else { + _state.postValue(ViewState.Success) + } } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt index 0bd5c660e1..412f2c33da 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardCourseItemViewModel.kt @@ -24,19 +24,25 @@ import com.instructure.pandautils.features.dashboard.edit.EditDashboardItemViewT import com.instructure.pandautils.mvvm.ItemViewModel class EditDashboardCourseItemViewModel( - val id: Long, - val name: String?, - @get:Bindable var isFavorite: Boolean, - val favoriteable: Boolean, - val openable: Boolean, - val termTitle: String?, - private val actionHandler: (EditDashboardItemAction) -> Unit + val id: Long, + val name: String?, + @get:Bindable var isFavorite: Boolean, + val favoritableOnline: Boolean, + val openable: Boolean, + val termTitle: String, + val online: Boolean, + val availableOffline: Boolean, + val enabled: Boolean, + private val actionHandler: (EditDashboardItemAction) -> Unit ) : ItemViewModel, BaseObservable() { override val layoutId: Int = R.layout.viewholder_edit_dashboard_course override val viewType: Int = EditDashboardItemViewType.COURSE.viewType + val favoritable: Boolean + get() = favoritableOnline && online + fun onClick() { if (!openable) { actionHandler(EditDashboardItemAction.ShowSnackBar(R.string.unauthorized)) @@ -47,15 +53,11 @@ class EditDashboardCourseItemViewModel( } fun onFavoriteClick() { - if (!favoriteable) { - actionHandler(EditDashboardItemAction.ShowSnackBar(R.string.inactive_courses_cant_be_added_to_dashboard)) - return - } - - if (isFavorite) { - actionHandler(EditDashboardItemAction.UnfavoriteCourse(this)) - } else { - actionHandler(EditDashboardItemAction.FavoriteCourse(this)) + when { + !online -> actionHandler(EditDashboardItemAction.ShowSnackBar(R.string.coursesCannotBeFavoritedOffline)) + !favoritableOnline -> actionHandler(EditDashboardItemAction.ShowSnackBar(R.string.inactive_courses_cant_be_added_to_dashboard)) + isFavorite -> actionHandler(EditDashboardItemAction.UnfavoriteCourse(this)) + else -> actionHandler(EditDashboardItemAction.FavoriteCourse(this)) } } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt index be69b26f54..0d2984cf51 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardHeaderViewModel.kt @@ -27,7 +27,8 @@ class EditDashboardHeaderViewModel( @get:StringRes val title: Int, @get:Bindable var hasItemSelected: Boolean, val selectAllHandler: () -> Unit, - val deselectAllHandler: () -> Unit + val deselectAllHandler: () -> Unit, + val online: Boolean ) : ItemViewModel, BaseObservable() { override val layoutId: Int = R.layout.viewholder_edit_dashboard_header diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardNoteItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardNoteItemViewModel.kt new file mode 100644 index 0000000000..394e244009 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/edit/itemviewmodels/EditDashboardNoteItemViewModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.dashboard.edit.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.edit.EditDashboardItemViewType +import com.instructure.pandautils.mvvm.ItemViewModel + +class EditDashboardNoteItemViewModel(val onCloseClicked: () -> Unit) : ItemViewModel { + override val layoutId: Int = R.layout.viewholder_edit_dashboard_note + + override val viewType: Int = EditDashboardItemViewType.NOTE.viewType +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt index b72c16b09a..91157e5568 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsFragment.kt @@ -110,6 +110,9 @@ class DashboardNotificationsFragment : Fragment() { is DashboardNotificationsActions.NavigateToMyFiles -> { dashboardRouter.routeToMyFiles(action.canvasContext, action.folderId) } + is DashboardNotificationsActions.OpenSyncProgress -> { + dashboardRouter.routeToSyncProgress() + } } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt index 612ed8c6da..75e3e4be6d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewData.kt @@ -22,16 +22,19 @@ import androidx.databinding.BaseObservable import androidx.databinding.Bindable import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Conference +import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.SyncProgressItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.UploadItemViewModel +import com.instructure.pandautils.features.offline.sync.ProgressState import com.instructure.pandautils.mvvm.ItemViewModel import java.util.* data class DashboardNotificationsViewData( val items: List, - var uploadItems: List + var uploadItems: List, + var syncProgressItems: SyncProgressItemViewModel? = null, ) : BaseObservable() { @Bindable - fun getConcatenatedItems() = uploadItems + items + fun getConcatenatedItems() = uploadItems + items + (syncProgressItems?.let { listOf(it) } ?: emptyList()) } data class InvitationViewData( @@ -62,6 +65,13 @@ data class UploadViewData( val isUploading: Boolean ) +data class SyncProgressViewData( + @Bindable var title: String = "", + @Bindable var subtitle: String = "", + @Bindable var progress: Int = 0, + @Bindable var progressState: ProgressState = ProgressState.STARTING, +) : BaseObservable() + sealed class DashboardNotificationsActions { data class ShowToast(val toast: String) : DashboardNotificationsActions() data class LaunchConference(val canvasContext: CanvasContext, val url: String) : DashboardNotificationsActions() @@ -73,4 +83,5 @@ sealed class DashboardNotificationsActions { val attemptId: Long ) : DashboardNotificationsActions() data class NavigateToMyFiles(val canvasContext: CanvasContext, val folderId: Long) : DashboardNotificationsActions() + object OpenSyncProgress: DashboardNotificationsActions() } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt index c8e284b1eb..d3a88809d9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardNotificationsViewModel.kt @@ -26,8 +26,18 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.await import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.managers.* -import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.managers.AccountNotificationManager +import com.instructure.canvasapi2.managers.ConferenceManager +import com.instructure.canvasapi2.managers.CourseManager +import com.instructure.canvasapi2.managers.EnrollmentManager +import com.instructure.canvasapi2.managers.GroupManager +import com.instructure.canvasapi2.managers.OAuthManager +import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.pandautils.BR @@ -35,8 +45,12 @@ import com.instructure.pandautils.R import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.AnnouncementItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.ConferenceItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.InvitationItemViewModel +import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.SyncProgressItemViewModel import com.instructure.pandautils.features.dashboard.notifications.itemviewmodels.UploadItemViewModel import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper +import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver +import com.instructure.pandautils.features.offline.sync.AggregateProgressViewData +import com.instructure.pandautils.features.offline.sync.ProgressState import com.instructure.pandautils.models.ConferenceDashboardBlacklist import com.instructure.pandautils.mvvm.Event import com.instructure.pandautils.mvvm.ItemViewModel @@ -44,12 +58,15 @@ import com.instructure.pandautils.mvvm.ViewState import com.instructure.pandautils.room.appdatabase.daos.DashboardFileUploadDao import com.instructure.pandautils.room.appdatabase.daos.FileUploadInputDao import com.instructure.pandautils.room.appdatabase.entities.DashboardFileUploadEntity +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao import com.instructure.pandautils.utils.orDefault import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.threeten.bp.OffsetDateTime -import java.util.* +import java.util.Locale +import java.util.UUID import javax.inject.Inject @HiltViewModel @@ -66,7 +83,10 @@ class DashboardNotificationsViewModel @Inject constructor( private val workManager: WorkManager, private val dashboardFileUploadDao: DashboardFileUploadDao, private val fileUploadInputDao: FileUploadInputDao, - private val fileUploadUtilsHelper: FileUploadUtilsHelper + private val fileUploadUtilsHelper: FileUploadUtilsHelper, + private val aggregateProgressObserver: AggregateProgressObserver, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao ) : ViewModel() { val state: LiveData @@ -92,14 +112,38 @@ class DashboardNotificationsViewModel @Inject constructor( } } + private val syncProgressObserver = Observer { aggregateProgressViewData -> + if (aggregateProgressViewData == null) { + _data.value?.syncProgressItems = null + _data.value?.notifyPropertyChanged(BR.concatenatedItems) + return@Observer + } + + if (_data.value?.syncProgressItems == null) { + _data.value?.syncProgressItems = createSyncProgressViewModel(aggregateProgressViewData) + _data.value?.notifyPropertyChanged(BR.concatenatedItems) + return@Observer + } + + if (aggregateProgressObserver.progressData.value?.progressState == ProgressState.COMPLETED) { + _data.value?.syncProgressItems = null + _data.value?.notifyPropertyChanged(BR.concatenatedItems) + } else { + _data.value?.syncProgressItems?.update(aggregateProgressViewData) + } + } + private val fileUploads = dashboardFileUploadDao.getAllForUser(apiPrefs.user?.id.orDefault()) init { fileUploads.observeForever(runningWorkersObserver) + aggregateProgressObserver.progressData.observeForever(syncProgressObserver) } override fun onCleared() { _data.value?.uploadItems?.forEach { it.clear() } + aggregateProgressObserver.progressData.removeObserver(syncProgressObserver) + aggregateProgressObserver.onCleared() fileUploads.removeObserver(runningWorkersObserver) super.onCleared() } @@ -127,10 +171,23 @@ class DashboardNotificationsViewModel @Inject constructor( val uploadViewModels = getUploads(fileUploads.value) - _data.postValue(DashboardNotificationsViewData(items, uploadViewModels)) + val syncProgress = aggregateProgressObserver.progressData.value?.let { + createSyncProgressViewModel(it) + } + + _data.postValue(DashboardNotificationsViewData(items, uploadViewModels, syncProgress)) } } + private fun getSyncProgress(aggregateProgressViewData: AggregateProgressViewData): SyncProgressItemViewModel { + return SyncProgressItemViewModel( + data = SyncProgressViewData(), + onClick = this::openSyncProgress, + onDismiss = this::dismissSyncProgress, + resources = resources + ).apply { update(aggregateProgressViewData) } + } + private suspend fun getAccountNotifications(forceNetwork: Boolean): List { val accountNotifications = accountNotificationManager.getAllAccountNotificationsAsync(forceNetwork).await().dataOrNull @@ -150,6 +207,7 @@ class DashboardNotificationsViewModel @Inject constructor( val icon = when (it.icon) { AccountNotification.ACCOUNT_NOTIFICATION_ERROR, AccountNotification.ACCOUNT_NOTIFICATION_WARNING -> R.drawable.ic_warning + AccountNotification.ACCOUNT_NOTIFICATION_CALENDAR -> R.drawable.ic_calendar AccountNotification.ACCOUNT_NOTIFICATION_QUESTION -> R.drawable.ic_question_mark else -> R.drawable.ic_info @@ -229,40 +287,43 @@ class DashboardNotificationsViewModel @Inject constructor( } } - private suspend fun getUploads(fileUploadEntities: List?) = fileUploadEntities?.mapNotNull { fileUploadEntity -> - val workerId = UUID.fromString(fileUploadEntity.workerId) - workManager.getWorkInfoById(workerId).await()?.let { workInfo -> - val icon: Int - val background: Int - when (workInfo.state) { - WorkInfo.State.FAILED -> { - icon = R.drawable.ic_exclamation_mark - background = R.color.backgroundDanger - } - WorkInfo.State.SUCCEEDED -> { - icon = R.drawable.ic_check_white_24dp - background = R.color.backgroundSuccess - } - else -> { - icon = R.drawable.ic_upload - background = R.color.backgroundInfo - } - } + private suspend fun getUploads(fileUploadEntities: List?) = + fileUploadEntities?.mapNotNull { fileUploadEntity -> + val workerId = UUID.fromString(fileUploadEntity.workerId) + workManager.getWorkInfoById(workerId).await()?.let { workInfo -> + val icon: Int + val background: Int + when (workInfo.state) { + WorkInfo.State.FAILED -> { + icon = R.drawable.ic_exclamation_mark + background = R.color.backgroundDanger + } - val uploadViewData = UploadViewData( - fileUploadEntity.title.orEmpty(), fileUploadEntity.subtitle.orEmpty(), - icon, background, workInfo.state == WorkInfo.State.RUNNING - ) + WorkInfo.State.SUCCEEDED -> { + icon = R.drawable.ic_check_white_24dp + background = R.color.backgroundSuccess + } - UploadItemViewModel( - workerId = workerId, - workManager = workManager, - data = uploadViewData, - open = { uuid -> openUploadNotification(workInfo.state, uuid, fileUploadEntity) }, - remove = { removeUploadNotification(fileUploadEntity, workerId) } - ) - } - }.orEmpty() + else -> { + icon = R.drawable.ic_upload + background = R.color.backgroundInfo + } + } + + val uploadViewData = UploadViewData( + fileUploadEntity.title.orEmpty(), fileUploadEntity.subtitle.orEmpty(), + icon, background, workInfo.state == WorkInfo.State.RUNNING + ) + + UploadItemViewModel( + workerId = workerId, + workManager = workManager, + data = uploadViewData, + open = { uuid -> openUploadNotification(workInfo.state, uuid, fileUploadEntity) }, + remove = { removeUploadNotification(fileUploadEntity, workerId) } + ) + } + }.orEmpty() private fun hasValidCourseForEnrollment(enrollment: Enrollment): Boolean { return coursesMap[enrollment.courseId]?.let { course -> @@ -304,7 +365,14 @@ class DashboardNotificationsViewModel @Inject constructor( } else if (fileUploadEntity.folderId != null) { dashboardFileUploadDao.delete(fileUploadEntity) apiPrefs.user?.let { - _events.postValue(Event(DashboardNotificationsActions.NavigateToMyFiles(it, fileUploadEntity.folderId))) + _events.postValue( + Event( + DashboardNotificationsActions.NavigateToMyFiles( + it, + fileUploadEntity.folderId + ) + ) + ) } } else { dashboardFileUploadDao.delete(fileUploadEntity) @@ -404,4 +472,23 @@ class DashboardNotificationsViewModel @Inject constructor( private fun openAnnouncement(subject: String, message: String) { _events.postValue(Event(DashboardNotificationsActions.OpenAnnouncement(subject, message))) } + + private fun openSyncProgress() { + _events.postValue(Event(DashboardNotificationsActions.OpenSyncProgress)) + } + + private fun dismissSyncProgress() { + viewModelScope.launch { + fileSyncProgressDao.deleteAll() + courseSyncProgressDao.deleteAll() + } + } + + private fun createSyncProgressViewModel(aggregateProgressViewData: AggregateProgressViewData): SyncProgressItemViewModel? { + return if (aggregateProgressViewData.progressState != ProgressState.COMPLETED) { + getSyncProgress(aggregateProgressViewData) + } else { + null + } + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardRouter.kt index ddeda96a2c..bd2039bf3e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardRouter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/DashboardRouter.kt @@ -25,4 +25,6 @@ interface DashboardRouter { fun routeToSubmissionDetails(canvasContext: CanvasContext, assignmentId: Long, attemptId: Long) fun routeToMyFiles(canvasContext: CanvasContext, folderId: Long) + + fun routeToSyncProgress() } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/SyncProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/SyncProgressItemViewModel.kt new file mode 100644 index 0000000000..bb198d5fa1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/dashboard/notifications/itemviewmodels/SyncProgressItemViewModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.dashboard.notifications.itemviewmodels + +import android.content.res.Resources +import com.instructure.pandautils.R +import com.instructure.pandautils.features.dashboard.notifications.SyncProgressViewData +import com.instructure.pandautils.features.offline.sync.AggregateProgressViewData +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.mvvm.ItemViewModel + +data class SyncProgressItemViewModel( + val data: SyncProgressViewData, + val onClick: () -> Unit, + val onDismiss: () -> Unit, + private val resources: Resources +) : ItemViewModel { + + override val layoutId: Int = R.layout.item_sync_progress + + fun update(progressData: AggregateProgressViewData) { + when (progressData.progressState) { + ProgressState.IN_PROGRESS -> { + data.title = resources.getString(R.string.syncProgress_syncingOfflineContent) + data.subtitle = resources.getQuantityString( + R.plurals.syncProgress_itemCount, + progressData.itemCount, + progressData.itemCount + ) + } + + ProgressState.ERROR -> { + data.title = resources.getString(R.string.syncProgress_offlineContentSyncFailed) + data.subtitle = resources.getString(R.string.syncProgress_syncErrorSubtitle) + } + + ProgressState.COMPLETED -> { + data.title = resources.getString(R.string.syncProgress_offlineContentSyncCompleted) + data.subtitle = "" + } + + ProgressState.STARTING -> { + data.title = resources.getString(R.string.syncProgress_downloadStarting) + data.subtitle = resources.getString(R.string.syncProgress_syncQueued) + } + } + + data.progress = progressData.progress + data.progressState = progressData.progressState + + data.notifyChange() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt index cf528824c9..8284162236 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt @@ -1,48 +1,31 @@ package com.instructure.pandautils.features.discussion.router -import com.instructure.canvasapi2.managers.DiscussionManager -import com.instructure.canvasapi2.managers.FeaturesManager -import com.instructure.canvasapi2.managers.GroupManager import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group -import com.instructure.pandautils.utils.FeatureFlagProvider -import com.instructure.pandautils.utils.isCourse -import com.instructure.pandautils.utils.isGroup +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.utils.orDefault class DiscussionRouteHelper( - private val featuresManager: FeaturesManager, - private val featureFlagProvider: FeatureFlagProvider, - private val discussionManager: DiscussionManager, - private val groupManager: GroupManager + private val discussionRouteHelperRepository: DiscussionRouteHelperRepository, ) { suspend fun isDiscussionRedesignEnabled(canvasContext: CanvasContext): Boolean { - return if (canvasContext.isCourse) { - val featureFlags = - featuresManager.getEnabledFeaturesForCourseAsync(canvasContext.id, false).await().dataOrNull - featureFlags?.contains("react_discussions_post") ?: false && featureFlagProvider.getDiscussionRedesignFeatureFlag() - } else if (canvasContext.isGroup) { - val featureFlags = - featuresManager.getEnabledFeaturesForCourseAsync((canvasContext as Group).courseId, false) - .await().dataOrNull - featureFlags?.contains("react_discussions_post") ?: false && featureFlagProvider.getDiscussionRedesignFeatureFlag() - } else { - false - } + return discussionRouteHelperRepository.getEnabledFeaturesForCourse(canvasContext, false) } suspend fun getDiscussionHeader( canvasContext: CanvasContext, discussionTopicHeaderId: Long - ): DiscussionTopicHeader { - return discussionManager.getDiscussionTopicHeaderAsync(canvasContext, discussionTopicHeaderId, false) - .await().dataOrThrow - } + ): DiscussionTopicHeader? { + return discussionRouteHelperRepository.getDiscussionTopicHeader(canvasContext, discussionTopicHeaderId, false) + } - suspend fun getDiscussionGroup(discussionTopicHeader: DiscussionTopicHeader): Pair? { - val groups = groupManager.getAllGroupsAsync(false).await().dataOrNull - for (group in groups ?: emptyList()) { + suspend fun getDiscussionGroup(discussionTopicHeader: DiscussionTopicHeader, user: User? = null): Pair? { + val userId = user?.id ?: ApiPrefs.user?.id.orDefault() + val groups = discussionRouteHelperRepository.getAllGroups(discussionTopicHeader, userId, false) + for (group in groups) { val groupsMap = discussionTopicHeader.groupTopicChildren.associateBy({ it.groupId }, { it.id }) if (groupsMap.contains(group.id) && groupsMap[group.id] != null) { groupsMap[group.id]?.let { topicHeaderId -> diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperDataSource.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperDataSource.kt new file mode 100644 index 0000000000..f0dd726ada --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.discussion.router + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group + +interface DiscussionRouteHelperDataSource { + + suspend fun getDiscussionTopicHeader(canvasContext: CanvasContext, discussionTopicHeaderId: Long, forceNetwork: Boolean): DiscussionTopicHeader? + suspend fun getAllGroups(discussionTopicHeader: DiscussionTopicHeader, userId: Long, forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperLocalDataSource.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperLocalDataSource.kt new file mode 100644 index 0000000000..2992045262 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperLocalDataSource.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.discussion.router + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade +import com.instructure.pandautils.room.offline.facade.GroupFacade + +class DiscussionRouteHelperLocalDataSource( + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + private val groupFacade: GroupFacade +) : DiscussionRouteHelperDataSource { + override suspend fun getDiscussionTopicHeader( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + forceNetwork: Boolean + ): DiscussionTopicHeader? { + return discussionTopicHeaderFacade.getDiscussionTopicHeaderById(discussionTopicHeaderId) + } + + override suspend fun getAllGroups( + discussionTopicHeader: DiscussionTopicHeader, + userId: Long, + forceNetwork: Boolean + ): List { + return groupFacade.getGroupsByUserId(userId) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt new file mode 100644 index 0000000000..69aecf616e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.discussion.router + +import com.instructure.canvasapi2.apis.DiscussionAPI +import com.instructure.canvasapi2.apis.FeaturesAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.isCourse +import com.instructure.pandautils.utils.isGroup + +class DiscussionRouteHelperNetworkDataSource( + private val discussionApi: DiscussionAPI.DiscussionInterface, + private val groupApi: GroupAPI.GroupInterface, + private val featuresApi: FeaturesAPI.FeaturesInterface, + private val featureFlagProvider: FeatureFlagProvider +) : DiscussionRouteHelperDataSource { + suspend fun getEnabledFeaturesForCourse(canvasContext: CanvasContext, forceNetwork: Boolean): Boolean { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return if (canvasContext.isCourse) { + val featureFlags = featuresApi.getEnabledFeaturesForCourse(canvasContext.id, params).dataOrNull + featureFlags?.contains("react_discussions_post") ?: false && featureFlagProvider.getDiscussionRedesignFeatureFlag() + } else if (canvasContext.isGroup) { + val group = canvasContext as Group + if (group.courseId == 0L) { + featureFlagProvider.getDiscussionRedesignFeatureFlag() + } else { + val featureFlags = featuresApi.getEnabledFeaturesForCourse(group.courseId, params).dataOrNull + featureFlags?.contains("react_discussions_post") ?: false && featureFlagProvider.getDiscussionRedesignFeatureFlag() + } + } else { + false + } + } + + override suspend fun getDiscussionTopicHeader( + canvasContext: CanvasContext, + discussionTopicHeaderId: Long, + forceNetwork: Boolean + ): DiscussionTopicHeader? { + val params = RestParams(isForceReadFromNetwork = forceNetwork) + return discussionApi.getDiscussionTopicHeader( + canvasContext.apiContext(), + canvasContext.id, + discussionTopicHeaderId, + params + ).dataOrNull + } + + override suspend fun getAllGroups( + discussionTopicHeader: DiscussionTopicHeader, + userId: Long, + forceNetwork: Boolean + ): List { + val params = RestParams(isForceReadFromNetwork = forceNetwork, usePerPageQueryParam = true) + return groupApi.getFirstPageGroups(params) + .depaginate { nextUrl -> groupApi.getNextPageGroups(nextUrl, params) }.dataOrNull.orEmpty() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt new file mode 100644 index 0000000000..de07ca8348 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt @@ -0,0 +1,11 @@ +package com.instructure.pandautils.features.discussion.router + +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group + +interface DiscussionRouteHelperRepository { + suspend fun getEnabledFeaturesForCourse(canvasContext: CanvasContext, forceNetwork: Boolean): Boolean + suspend fun getDiscussionTopicHeader(canvasContext: CanvasContext, discussionTopicHeaderId: Long, forceNetwork: Boolean): DiscussionTopicHeader? + suspend fun getAllGroups(discussionTopicHeader: DiscussionTopicHeader, userId: Long, forceNetwork: Boolean): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt index bcb04d563a..fb5861937e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt @@ -36,12 +36,12 @@ class DiscussionRouterViewModel @Inject constructor( val header = discussionTopicHeader ?: discussionRouteHelper.getDiscussionHeader( canvasContext, discussionTopicHeaderId - ) + )!! if (header.groupTopicChildren.isNotEmpty()) { val discussionGroup = discussionRouteHelper.getDiscussionGroup(header) discussionGroup?.let { - val groupDiscussionHeader = discussionRouteHelper.getDiscussionHeader(it.first, it.second) + val groupDiscussionHeader = discussionRouteHelper.getDiscussionHeader(it.first, it.second)!! routeToDiscussionGroup( it.first, it.second, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt index 1eaa816530..0bf7e24476 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/homeroom/HomeroomFragment.kt @@ -142,7 +142,6 @@ class HomeroomFragment : Fragment() { private fun setupWebView(announcementWebView: CanvasWebView) { WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) announcementWebView.setBackgroundColor(requireContext().getColor(R.color.backgroundLightest)) - announcementWebView.settings.allowFileAccess = true announcementWebView.settings.loadWithOverviewMode = true announcementWebView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun routeInternallyCallback(url: String) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt index c49f295ca1..ee13935fb8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/elementary/resources/ResourcesFragment.kt @@ -113,7 +113,6 @@ class ResourcesFragment : Fragment() { private fun setupWebView(webView: CanvasWebView) { WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) webView.setBackgroundColor(requireContext().getColor(R.color.backgroundLightest)) - webView.settings.allowFileAccess = true webView.settings.loadWithOverviewMode = true webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun routeInternallyCallback(url: String) { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/download/FileDownloadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/download/FileDownloadWorker.kt index 954d39fdd5..8445665638 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/download/FileDownloadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/download/FileDownloadWorker.kt @@ -35,6 +35,7 @@ import androidx.work.WorkerParameters import com.instructure.canvasapi2.apis.DownloadState import com.instructure.canvasapi2.apis.FileDownloadAPI import com.instructure.canvasapi2.apis.saveFile +import com.instructure.canvasapi2.builders.RestParams import com.instructure.pandautils.R import com.instructure.pandautils.features.file.upload.worker.FileUploadWorker import dagger.assisted.Assisted @@ -70,29 +71,36 @@ class FileDownloadWorker @AssistedInject constructor( } var result = Result.retry() - fileDownloadApi.downloadFile(fileUrl).saveFile(downloadedFile) - .collect { downloadState -> - when (downloadState) { - is DownloadState.InProgress -> { - foregroundInfo = createForegroundInfo(notificationId, fileName, downloadState.progress) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - setForeground(foregroundInfo) - } else { - updateForegroundNotification() + try { + fileDownloadApi.downloadFile(fileUrl, RestParams()) + .dataOrThrow + .saveFile(downloadedFile) + .collect { downloadState -> + when (downloadState) { + is DownloadState.InProgress -> { + foregroundInfo = createForegroundInfo(notificationId, fileName, downloadState.progress) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + setForeground(foregroundInfo) + } else { + updateForegroundNotification() + } } - } - is DownloadState.Failure -> { - result = Result.failure() - updateNotificationFailed(notificationId, fileName) - } + is DownloadState.Failure -> { + throw downloadState.throwable + } - is DownloadState.Success -> { - result = Result.success() - updateNotificationComplete(notificationId, fileName) + is DownloadState.Success -> { + result = Result.success() + updateNotificationComplete(notificationId, fileName) + } } } - } + } catch (e: Exception) { + result = Result.failure() + updateNotificationFailed(notificationId, fileName) + } + return result } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt index a0bc0b9c25..4799e6bf9f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/file/upload/worker/FileUploadWorker.kt @@ -37,14 +37,6 @@ import com.instructure.pandautils.R import com.instructure.pandautils.features.file.upload.FileUploadUtilsHelper import com.instructure.pandautils.room.appdatabase.daos.* import com.instructure.pandautils.room.appdatabase.entities.* -import com.instructure.pandautils.room.common.daos.AttachmentDao -import com.instructure.pandautils.room.common.daos.AuthorDao -import com.instructure.pandautils.room.common.daos.MediaCommentDao -import com.instructure.pandautils.room.common.daos.SubmissionCommentDao -import com.instructure.pandautils.room.common.entities.AttachmentEntity -import com.instructure.pandautils.room.common.entities.AuthorEntity -import com.instructure.pandautils.room.common.entities.MediaCommentEntity -import com.instructure.pandautils.room.common.entities.SubmissionCommentEntity import com.instructure.pandautils.utils.FileUploadUtils import com.instructure.pandautils.utils.orDefault import com.instructure.pandautils.utils.toJson diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/CourseFileSharedRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/CourseFileSharedRepository.kt new file mode 100644 index 0000000000..7476f4704c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/CourseFileSharedRepository.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent + +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.depaginate + +class CourseFileSharedRepository(private val fileFolderApi: FileFolderAPI.FilesFoldersInterface) { + + suspend fun getCourseFoldersAndFiles(courseId: Long): List { + val params = RestParams(isForceReadFromNetwork = true) + val rootFolderResult = + fileFolderApi.getRootFolderForContext(courseId, CanvasContext.Type.COURSE.apiString, params) + + if (rootFolderResult.isFail) return emptyList() + + val result = mutableListOf() + + result.addAll(getAllFoldersAndFiles(rootFolderResult.dataOrThrow)) + + return result + } + + private suspend fun getAllFoldersAndFiles(folder: FileFolder): List { + val result = mutableListOf() + result.add(folder) + val subFolders = getFolders(folder) + + val currentFolderFiles = getFiles(folder) + result.addAll(currentFolderFiles) + + for (subFolder in subFolders) { + val subFolderFiles = getAllFoldersAndFiles(subFolder) + result.addAll(subFolderFiles) + } + + return result + } + + suspend fun getCourseFiles(courseId: Long): List { + val params = RestParams(isForceReadFromNetwork = true) + val rootFolderResult = + fileFolderApi.getRootFolderForContext(courseId, CanvasContext.Type.COURSE.apiString, params) + + if (rootFolderResult.isFail) return emptyList() + + return getAllFiles(rootFolderResult.dataOrThrow) + } + + private suspend fun getAllFiles(folder: FileFolder): List { + val result = mutableListOf() + val subFolders = getFolders(folder) + + val currentFolderFiles = getFiles(folder) + result.addAll(currentFolderFiles) + + for (subFolder in subFolders) { + val subFolderFiles = getAllFiles(subFolder) + result.addAll(subFolderFiles) + } + + return result + } + + private suspend fun getFolders(folder: FileFolder): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val foldersResult = fileFolderApi.getFirstPageFolders(folder.id, params).depaginate { nextUrl -> + fileFolderApi.getNextPageFileFoldersList(nextUrl, params) + } + + return foldersResult.dataOrNull.orEmpty().filterValidFileFolders() + } + + private suspend fun getFiles(folder: FileFolder): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val filesResult = fileFolderApi.getFirstPageFiles(folder.id, params).depaginate { nextUrl -> + fileFolderApi.getNextPageFileFoldersList(nextUrl, params) + } + + return filesResult.dataOrNull.orEmpty().filterValidFileFolders() + } + + private fun List.filterValidFileFolders() = this.filter { + !it.isHidden && !it.isLocked && !it.isHiddenForUser && !it.isLockedForUser + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentFragment.kt new file mode 100644 index 0000000000..e516792a45 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentFragment.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.interactions.FragmentInteractions +import com.instructure.interactions.Navigation +import com.instructure.interactions.router.Route +import com.instructure.pandautils.R +import com.instructure.pandautils.databinding.FragmentOfflineContentBinding +import com.instructure.pandautils.utils.* +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class OfflineContentFragment : Fragment(), FragmentInteractions { + + private val viewModel: OfflineContentViewModel by viewModels() + + private var canvasContext: CanvasContext? by NullableParcelableArg(key = Const.CANVAS_CONTEXT) + + private lateinit var binding: FragmentOfflineContentBinding + + private val backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewModel.shouldShowDiscardDialog()) { + showDialog( + getString(R.string.offline_content_discard_dialog_title), + getString(R.string.offline_content_discard_dialog_message), + getString(R.string.offline_content_discard_dialog_positive) + ) { + navigateBack() + } + } else { + navigateBack() + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentOfflineContentBinding.inflate(inflater, container, false).apply { + lifecycleOwner = this@OfflineContentFragment + viewModel = this@OfflineContentFragment.viewModel + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + applyTheme() + + viewModel.data.observe(viewLifecycleOwner) { data -> + updateMenuText(data.selectedCount) + } + + viewModel.events.observe(viewLifecycleOwner) { event -> + event.getContentIfNotHandled()?.let { + handleAction(it) + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + activity?.onBackPressedDispatcher?.addCallback(this, backPressedCallback) + } + + private fun navigateBack() { + backPressedCallback.remove() + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + private fun handleAction(action: OfflineContentAction) { + when (action) { + is OfflineContentAction.Back -> navigateBack() + is OfflineContentAction.Dialog -> showDialog( + title = action.title, + message = action.message, + positive = action.positive, + positiveCallback = action.positiveCallback + ) + } + } + + override val navigation: Navigation? + get() = activity as? Navigation + + override fun title(): String = getString(R.string.offline_content_toolbar_title) + + override fun applyTheme() { + ViewStyler.themeToolbarColored(requireActivity(), binding.toolbar, ThemePrefs.primaryColor, ThemePrefs.primaryTextColor) + binding.toolbar.apply { + subtitle = canvasContext?.name ?: getString(R.string.offline_content_all_courses) + setBackgroundColor(ThemePrefs.primaryColor) + setupAsBackButton(this@OfflineContentFragment) + setMenu(R.menu.menu_offline_content) { + viewModel.toggleSelection() + } + } + + viewModel.data.value?.let { data -> + updateMenuText(data.selectedCount) + } + } + + override fun getFragment(): Fragment = this + + private fun showDialog(title: String, message: String, positive: String, positiveCallback: () -> Unit) { + AlertDialog.Builder(requireContext()) + .setTitle(title) + .setMessage(message) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(positive) { _, _ -> positiveCallback() } + .showThemed() + } + + private fun updateMenuText(selectedCount: Int) { + binding.toolbar.menu.items.firstOrNull()?.title = getString( + if (selectedCount > 0) R.string.offline_content_deselect_all else R.string.offline_content_select_all + ) + } + + companion object { + + fun makeRoute(canvasContext: CanvasContext? = null) = Route(OfflineContentFragment::class.java, canvasContext) + + private fun validRoute(route: Route) = route.primaryClass == OfflineContentFragment::class.java + + fun newInstance(route: Route) = if (validRoute(route)) OfflineContentFragment().withArgs(route.argsWithContext) else null + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentRepository.kt new file mode 100644 index 0000000000..3d89eeb8e4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentRepository.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.canvasapi2.utils.hasActiveEnrollment +import com.instructure.canvasapi2.utils.isValidTerm +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.SyncSettingsEntity +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles + +class OfflineContentRepository( + private val coursesApi: CourseAPI.CoursesInterface, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val fileSyncSettingsDao: FileSyncSettingsDao, + private val courseFileSharedRepository: CourseFileSharedRepository, + private val syncSettingsFacade: SyncSettingsFacade, + private val localFileDao: LocalFileDao, + private val fileSyncProgressDao: FileSyncProgressDao +) { + suspend fun getCourse(courseId: Long): Course { + val params = RestParams(isForceReadFromNetwork = true) + val courseResult = coursesApi.getCourse(courseId, params) + + return courseResult.dataOrThrow + } + + suspend fun getCourses(): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val coursesResult = coursesApi.getFirstPageCourses(params).depaginate { nextUrl -> coursesApi.next(nextUrl, params) } + + return coursesResult.dataOrThrow.filter { it.isValidTerm() && it.hasActiveEnrollment() } + } + + suspend fun getCourseFiles(courseId: Long): List { + return courseFileSharedRepository.getCourseFiles(courseId) + } + + suspend fun findCourseSyncSettings(course: Course): CourseSyncSettingsWithFiles { + var courseSettingsWithFiles = courseSyncSettingsDao.findWithFilesById(course.id) + if (courseSettingsWithFiles == null) { + val default = CourseSyncSettingsEntity(course.id, course.name, false) + courseSyncSettingsDao.insert(default) + + courseSettingsWithFiles = CourseSyncSettingsWithFiles( + default, + emptyList() + ) + + } + return courseSettingsWithFiles + } + + suspend fun updateCourseSyncSettings( + courseId: Long, + courseSyncSettings: CourseSyncSettingsEntity, + fileSyncSettings: List + ) { + courseSyncSettingsDao.update(courseSyncSettings) + fileSyncSettingsDao.updateCourseFiles(courseId, fileSyncSettings) + } + + suspend fun saveFileSettings(fileSyncSettingsEntity: FileSyncSettingsEntity) { + fileSyncSettingsDao.insert(fileSyncSettingsEntity) + } + + suspend fun deleteFileSettings(fileId: Long) { + fileSyncSettingsDao.deleteById(fileId) + } + + suspend fun deleteFileSettings(fileIds: List) { + fileSyncSettingsDao.deleteByIds(fileIds) + } + + suspend fun getSyncSettings(): SyncSettingsEntity { + return syncSettingsFacade.getSyncSettings() + } + + suspend fun isFileSynced(fileId: Long): Boolean { + return localFileDao.existsById(fileId) + } + + suspend fun getInProgressFileSize(fileId: Long): Long { + val file = fileSyncProgressDao.findByFileId(fileId) ?: return 0 + return (file.fileSize * (file.progress / 100.0)).toLong() + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewData.kt new file mode 100644 index 0000000000..89b1a1b0f1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewData.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent + +import androidx.databinding.BaseObservable +import com.google.android.material.checkbox.MaterialCheckBox +import com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels.CourseItemViewModel +import com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels.CourseTabViewModel +import com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels.FileViewModel + +data class OfflineContentViewData( + val storageInfo: StorageInfo, + val courseItems: List, + val selectedCount: Int +) : BaseObservable() + +data class StorageInfo(val otherAppsReservedPercent: Int, val allAppsReservedPercent: Int, val storageInfoText: String) + +data class CourseItemViewData( + var fullContentSync: Boolean, + val title: String, + val size: String, + val tabs: List +) : BaseObservable() { + + fun checkedState(): Int { + return when { + fullContentSync -> MaterialCheckBox.STATE_CHECKED + tabs.isNotEmpty() && tabs.all { it.data.synced } -> MaterialCheckBox.STATE_CHECKED + tabs.any { it.data.synced || it.data.files.any { file -> file.data.checked } } -> MaterialCheckBox.STATE_INDETERMINATE + else -> MaterialCheckBox.STATE_UNCHECKED + } + } +} + +data class CourseTabViewData( + var synced: Boolean, + val title: String, + val size: String, + val files: List +) : BaseObservable() { + + fun checkedState(): Int { + return when { + synced -> MaterialCheckBox.STATE_CHECKED + files.isNotEmpty() && files.all { it.data.checked } -> MaterialCheckBox.STATE_CHECKED + files.any { it.data.checked } -> MaterialCheckBox.STATE_INDETERMINATE + else -> MaterialCheckBox.STATE_UNCHECKED + } + } +} + +data class FileViewData( + var checked: Boolean, + val title: String, + val size: String +) : BaseObservable() + +enum class OfflineItemViewModelType(val viewType: Int) { + COURSE(1), + COURSE_TAB(2), + FILE(3), + EMPTY_COURSE_CONTENT(4) +} + +sealed class OfflineContentAction { + object Back : OfflineContentAction() + data class Dialog( + val title: String, + val message: String, + val positive: String, + val positiveCallback: () -> Unit + ) : OfflineContentAction() +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt new file mode 100644 index 0000000000..18323b2c61 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/OfflineContentViewModel.kt @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent + +import android.content.Context +import android.text.format.Formatter +import androidx.lifecycle.* +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels.CourseItemViewModel +import com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels.CourseTabViewModel +import com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels.FileViewModel +import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.StorageUtils +import com.instructure.pandautils.utils.orDefault +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.collections.set + +private val ALLOWED_TAB_IDS = CourseSyncSettingsEntity.TABS.plus(Tab.FILES_ID) +private const val TAB_SIZE = 100000 + +@HiltViewModel +class OfflineContentViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + @ApplicationContext private val context: Context, + private val offlineContentRepository: OfflineContentRepository, + private val storageUtils: StorageUtils, + private val offlineSyncHelper: OfflineSyncHelper +) : ViewModel() { + + val course = savedStateHandle.get(Const.CANVAS_CONTEXT) + + val state: LiveData + get() = _state + private val _state = MutableLiveData() + + val data: LiveData + get() = _data + private val _data = MutableLiveData() + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + private val syncSettingsMap = mutableMapOf() + private val originalSyncSettingsMap = mutableMapOf() + + private val courseMap = mutableMapOf() + private val courseFilesMap = mutableMapOf>() + private val courseSelectedFilesMap = mutableMapOf>() + + init { + loadData() + } + + private fun loadData(isRefresh: Boolean = false) { + _state.postValue( + ViewState.LoadingWithAnimation( + R.string.offline_content_sync_loading_title, + R.string.offline_content_sync_loading_message, + R.raw.snail + ) + ) + if (isRefresh) { + _data.value = _data.value?.copy(courseItems = emptyList(), selectedCount = 0) + } + viewModelScope.launch { + try { + val storageInfo = getStorageInfo() + courseMap.putAll(getCourses(course?.id).associateBy { it.id }) + courseFilesMap.putAll(getCourseFiles(courseMap.values.toList())) + syncSettingsMap.putAll(getCourseSyncSettings(courseMap.values.toList())) + originalSyncSettingsMap.putAll(getCourseSyncSettings(courseMap.values.toList())) + courseSelectedFilesMap.putAll(getSelectedFiles(courseMap.values.toList())) + val coursesData = createCourseItemViewModels() + val data = OfflineContentViewData(storageInfo, coursesData, getSelectedItemCount(coursesData)) + _data.postValue(data) + if (coursesData.isEmpty()) { + _state.postValue( + ViewState.Empty( + R.string.offline_content_empty_title, + R.string.offline_content_empty_message, + R.drawable.ic_panda_space + ) + ) + } else { + _state.postValue(ViewState.Success) + } + } catch (ex: Exception) { + _state.postValue(ViewState.Error(context.getString(R.string.offline_content_loading_error))) + } + } + } + + private fun getSelectedFiles(courses: List): List>> { + return courses.map { course -> + val courseFilesMap = courseFilesMap[course.id] ?: throw IllegalStateException() + val syncSettings = syncSettingsMap[course.id] ?: throw IllegalStateException() + val fileSyncSettings = syncSettings.files + + if (syncSettings.courseSyncSettings.fullFileSync) { + course.id to courseFilesMap.values.map { it.id }.toMutableSet() + } else { + course.id to fileSyncSettings.map { + it.id + }.toMutableSet() + } + } + } + + private suspend fun getCourseSyncSettings(courses: List): List> { + return courses.map { + it.id to offlineContentRepository.findCourseSyncSettings(it) + } + } + + private suspend fun getCourses(courseId: Long?): List { + return if (courseId == null) { + offlineContentRepository.getCourses() + } else { + listOf(offlineContentRepository.getCourse(courseId)) + } + } + + private suspend fun getCourseFiles(courses: List): List>> { + return courses.map { course -> + val files = if (course.tabs?.any { it.tabId == Tab.FILES_ID } == true) { + offlineContentRepository.getCourseFiles(course.id) + } else emptyList() + + val filesMap = files.associateBy { it.id } + course.id to filesMap + } + } + + private fun createCourseItemViewModels(): List { + val courses = courseMap.values + + return courses.map { course -> + createCourseItemViewModel(course.id) + } + } + + private fun createCourseItemViewModel( + courseId: Long + ): CourseItemViewModel { + val course = courseMap[courseId] ?: throw IllegalStateException() + val courseSyncSettingsWithFiles = syncSettingsMap[courseId] ?: throw IllegalStateException() + + val files = courseFilesMap[course.id]?.values ?: emptyList() + val tabs = course.tabs?.filter { it.tabId in ALLOWED_TAB_IDS }.orEmpty() + val size = "~${Formatter.formatShortFileSize(context, files.sumOf { it.size } + tabs.filter { it.tabId != Tab.FILES_ID }.size * TAB_SIZE)}" + + val collapsed = _data.value?.courseItems?.find { it.courseId == courseId }?.collapsed ?: (this.course == null) + + return CourseItemViewModel( + data = CourseItemViewData( + fullContentSync = courseSyncSettingsWithFiles.courseSyncSettings.fullContentSync, + title = course.name, + size = size, + tabs = tabs.map { tab -> + createTabViewModel( + course.id, + tab + ) + } + ), + courseId = course.id, + collapsed = collapsed, + onCheckedChanged = this::onCourseCheckedChanged + ) + } + + private fun onCourseCheckedChanged(checked: Boolean, courseItemViewModel: CourseItemViewModel) { + data.value?.courseItems?.find { it == courseItemViewModel } ?: return + + toggleCourse(courseItemViewModel.courseId, checked) + + val newCourseViewModel = createCourseItemViewModel(courseItemViewModel.courseId) + val newList = _data.value?.courseItems?.map { if (it == courseItemViewModel) newCourseViewModel else it }.orEmpty() + + val selectedCount = getSelectedItemCount(newList) + + viewModelScope.launch { + _data.value = _data.value?.copy(storageInfo = getStorageInfo(), courseItems = newList, selectedCount = selectedCount) + } + } + + private fun createTabViewModel( + courseId: Long, + tab: Tab + ): CourseTabViewModel { + val isFilesTab = tab.tabId == Tab.FILES_ID + val courseSyncSettingsWithFiles = syncSettingsMap[courseId] ?: throw IllegalArgumentException() + val files = if (isFilesTab) courseFilesMap[courseId]?.values.orEmpty() else emptyList() + val size = if (isFilesTab) Formatter.formatShortFileSize(context, files.sumOf { it.size }) else null + + val collapsed = _data.value?.courseItems?.find { it.courseId == courseId }?.data?.tabs?.find { it.tabId == Tab.FILES_ID }?.collapsed ?: false + + return CourseTabViewModel( + data = CourseTabViewData( + synced = courseSyncSettingsWithFiles.courseSyncSettings.isTabSelected(tab.tabId), + title = tab.label.orEmpty(), + size = size ?: "", + files = files.map { fileFolder -> + createFileViewModel( + fileFolder, + courseId, + tab.tabId, + courseSyncSettingsWithFiles + ) + }), + courseId = courseId, + tabId = tab.tabId, + collapsed = collapsed, + onCheckedChanged = this::onTabCheckedChanged + ) + } + + private fun createFileViewModel( + fileFolder: FileFolder, + courseId: Long, + tabId: String, + syncSettingsWithFiles: CourseSyncSettingsWithFiles + ): FileViewModel { + val fileSize = Formatter.formatShortFileSize(context, fileFolder.size) + val isChecked = syncSettingsWithFiles.courseSyncSettings.fullFileSync || courseSelectedFilesMap[courseId]?.contains(fileFolder.id) ?: false + + return FileViewModel( + data = FileViewData( + checked = isChecked, + title = fileFolder.displayName.orEmpty(), + size = fileSize + ), + courseId = courseId, + fileId = fileFolder.id, + fileUrl = fileFolder.url, + tabId = tabId, + onCheckedChanged = this::fileCheckedChanged + ) + } + + private fun onTabCheckedChanged(checked: Boolean, tabItemViewModel: CourseTabViewModel) { + val courseViewModel = data.value?.courseItems?.find { it.courseId == tabItemViewModel.courseId } ?: return + + updateTab(tabItemViewModel.courseId, tabItemViewModel.tabId, checked) + val newCourseViewModel = createCourseItemViewModel(courseViewModel.courseId) + val newList = _data.value?.courseItems?.map { if (it == courseViewModel) newCourseViewModel else it }.orEmpty() + + val selectedCount = getSelectedItemCount(newList) + + viewModelScope.launch { + _data.value = _data.value?.copy(storageInfo = getStorageInfo(), courseItems = newList, selectedCount = selectedCount) + } + } + + private fun updateTab(courseId: Long, tabId: String, checked: Boolean) { + val courseSettingWithFiles = syncSettingsMap[courseId] ?: return + val courseSyncSettings = courseSettingWithFiles.courseSyncSettings + + var updated = if (tabId == Tab.FILES_ID) { + toggleAllFiles(courseId, checked) + courseSyncSettings.copy(fullFileSync = checked) + } else { + val newTabs = courseSyncSettings.tabs.plus(Pair(tabId, checked)) + courseSyncSettings.copy(tabs = newTabs) + } + updated = updated.copy(fullContentSync = updated.allTabsEnabled) + + val updatedSyncSettings = courseSettingWithFiles.copy( + courseSyncSettings = updated + ) + + syncSettingsMap[courseId] = updatedSyncSettings + } + + private fun fileCheckedChanged(checked: Boolean, item: FileViewModel) { + val courseViewModel = data.value?.courseItems?.find { it.courseId == item.courseId } ?: return + + updateFile(checked, courseViewModel.courseId, item.fileId) + + val newCourseViewModel = createCourseItemViewModel(item.courseId) + val newList = _data.value?.courseItems?.map { if (it == courseViewModel) newCourseViewModel else it }.orEmpty() + val selectedCount = getSelectedItemCount(newList) + + viewModelScope.launch { + _data.value = _data.value?.copy(storageInfo = getStorageInfo(), courseItems = newList, selectedCount = selectedCount) + } + } + + private fun updateFile(checked: Boolean, courseId: Long, fileId: Long) { + if (checked) { + courseSelectedFilesMap[courseId]?.add(fileId) + } else { + courseSelectedFilesMap[courseId]?.remove(fileId) + } + + val syncSettings = syncSettingsMap[courseId] ?: return + + var updated = syncSettings.courseSyncSettings.copy( + fullFileSync = courseSelectedFilesMap[courseId]?.size == courseFilesMap[courseId]?.size + ) + updated = updated.copy(fullContentSync = updated.allTabsEnabled) + + syncSettingsMap[courseId] = syncSettings.copy(courseSyncSettings = updated) + } + + private fun getSelectedItemCount(courses: List): Int { + val selectedTabs = courses.flatMap { it.data.tabs }.count { it.data.synced && it.tabId != Tab.FILES_ID } + val selectedFiles = courses.flatMap { it.data.tabs }.flatMap { it.data.files }.count { it.data.checked } + return selectedTabs + selectedFiles + } + + fun toggleSelection() { + val shouldCheck = _data.value?.selectedCount.orDefault() == 0 + _data.value?.courseItems?.forEach { + toggleCourse(it.courseId, shouldCheck) + } + + val newList = createCourseItemViewModels() + val selectedCount = getSelectedItemCount(newList) + + viewModelScope.launch { + _data.value = _data.value?.copy(storageInfo = getStorageInfo(), courseItems = newList, selectedCount = selectedCount) + } + } + + private fun toggleCourse(courseId: Long, shouldCheck: Boolean) { + val syncSettingsWithFiles = syncSettingsMap[courseId] ?: return + val courseSyncSettings = syncSettingsWithFiles.courseSyncSettings.copy( + fullContentSync = shouldCheck, + tabs = CourseSyncSettingsEntity.TABS.associateWith { shouldCheck }, + fullFileSync = shouldCheck + ) + + val updatedSyncSettings = syncSettingsWithFiles.copy( + courseSyncSettings = courseSyncSettings + ) + + toggleAllFiles(courseId, shouldCheck) + + syncSettingsMap[courseId] = updatedSyncSettings + } + + private fun toggleAllFiles(courseId: Long, shouldCheck: Boolean) { + if (shouldCheck) { + courseSelectedFilesMap[courseId]?.addAll(courseFilesMap[courseId]?.values.orEmpty().map { it.id }) + } else { + courseSelectedFilesMap[courseId]?.clear() + } + } + + fun onSyncClicked() { + viewModelScope.launch { + val wifiOnly = offlineContentRepository.getSyncSettings().wifiOnly + _events.postValue( + Event( + OfflineContentAction.Dialog( + context.getString(R.string.offline_content_sync_dialog_title), + context.getString( + if (wifiOnly) R.string.offline_content_sync_dialog_message_wifi_only + else R.string.offline_content_sync_dialog_message, + Formatter.formatShortFileSize(context, getSelectedSize()) + ), + context.getString(R.string.offline_content_sync_dialog_positive), + ::startSync + ) + ) + ) + } + } + + private fun getSelectedSize(): Long { + val tabSize = courseMap.values.sumOf { course -> + course.tabs?.filter { it.tabId in ALLOWED_TAB_IDS && it.tabId != Tab.FILES_ID }?.count { + syncSettingsMap[course.id]?.courseSyncSettings?.isTabSelected(it.tabId).orDefault() + }.orDefault() * TAB_SIZE + } + + val fileSize = courseSelectedFilesMap.values.flatten().sumOf { selectedId -> + courseFilesMap.values.sumOf { + it[selectedId]?.size.orDefault() + } + } + + return tabSize + fileSize + } + + private fun startSync() { + viewModelScope.launch { + saveSettings() + offlineSyncHelper.syncCourses(syncSettingsMap.keys.toList()) + _events.postValue(Event(OfflineContentAction.Back)) + } + } + + private suspend fun saveSettings() { + syncSettingsMap.forEach { courseSettingsEntry -> + val courseId = courseSettingsEntry.key + val fileSettings = courseSelectedFilesMap[courseId].orEmpty().mapNotNull { fileId -> + courseFilesMap[courseId]?.get(fileId)?.let { + FileSyncSettingsEntity(it.id, it.displayName, courseId, it.url) + } + } + offlineContentRepository.updateCourseSyncSettings(courseSettingsEntry.key, courseSettingsEntry.value.courseSyncSettings, fileSettings) + } + } + + fun onRefresh() { + loadData(true) + } + + fun shouldShowDiscardDialog(): Boolean { + return syncSettingsMap != originalSyncSettingsMap + } + + private suspend fun getStorageInfo(): StorageInfo { + val appSizeModifier = getAppSizeModifier() + val appSize = storageUtils.getAppSize() + appSizeModifier + val totalSpace = storageUtils.getTotalSpace() + val usedSpace = totalSpace - (storageUtils.getFreeSpace() - appSizeModifier) + val otherAppsSpace = usedSpace - appSize + val otherPercent = if (totalSpace > 0) (otherAppsSpace.toFloat() / totalSpace * 100).toInt() else 0 + val canvasPercent = if (totalSpace > 0) (appSize.toFloat() / totalSpace * 100).toInt().coerceAtLeast(1) + otherPercent else 0 + val storageInfoText = context.getString( + R.string.offline_content_storage_info, + Formatter.formatShortFileSize(context, usedSpace), + Formatter.formatShortFileSize(context, totalSpace), + ) + + return StorageInfo(otherPercent, canvasPercent, storageInfoText) + } + + private suspend fun getAppSizeModifier(): Long { + var modifier = 0L + originalSyncSettingsMap.forEach { syncSetting -> + courseMap[syncSetting.key]?.tabs?.filter { it.tabId in ALLOWED_TAB_IDS }?.forEach { tab -> + if (tab.tabId != Tab.FILES_ID) { + val original = syncSetting.value.courseSyncSettings.isTabSelected(tab.tabId) + syncSettingsMap[syncSetting.key]?.courseSyncSettings?.isTabSelected(tab.tabId)?.let { actual -> + if (original && !actual) { + modifier -= TAB_SIZE + } else if (!original && actual) { + modifier += TAB_SIZE + } + } + } + } + + courseFilesMap[syncSetting.key]?.forEach { + val original = syncSetting.value.isFileSelected(it.value.id) + courseSelectedFilesMap[syncSetting.key]?.contains(it.value.id)?.let { actual -> + if (original && !actual) { + modifier -= getActualLocalFileSize(it.value) + } else if (!original && actual) { + modifier += it.value.size + } + } + } + } + + return modifier + } + + private suspend fun getActualLocalFileSize(fileFolder: FileFolder): Long { + return if (offlineContentRepository.isFileSynced(fileFolder.id)) { + fileFolder.size + } else { + offlineContentRepository.getInProgressFileSize(fileFolder.id) + } + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt new file mode 100644 index 0000000000..7fa6a016d6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseItemViewModel.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels + +import android.widget.CompoundButton.OnCheckedChangeListener +import androidx.databinding.Bindable +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.offline.offlinecontent.CourseItemViewData +import com.instructure.pandautils.features.offline.offlinecontent.OfflineItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +data class CourseItemViewModel( + val data: CourseItemViewData, + val courseId: Long, + @get:Bindable override var collapsed: Boolean, + val onCheckedChanged: (Boolean, CourseItemViewModel) -> Unit +) : GroupItemViewModel(collapsable = true, items = data.tabs.ifEmpty { listOf(EmptyCourseContentViewModel()) }) { + override val layoutId = R.layout.item_offline_course + override val viewType = OfflineItemViewModelType.COURSE.viewType + + val onCheckChanged = OnCheckedChangeListener { cb, checked -> + if (cb.isPressed) onCheckedChanged(checked, this) + } + + fun onRowClicked() { + data.fullContentSync = !data.fullContentSync + onCheckedChanged(data.fullContentSync, this) + } + + override fun areContentsTheSame(other: ItemViewModel): Boolean { + return other is CourseItemViewModel + && other.courseId == this.courseId + && other.data == this.data + } + + override fun areItemsTheSame(other: ItemViewModel): Boolean { + return other is CourseItemViewModel && other.courseId == this.courseId + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseTabViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseTabViewModel.kt new file mode 100644 index 0000000000..c50e5de86f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/CourseTabViewModel.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels + +import android.widget.CompoundButton +import androidx.databinding.Bindable +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.offline.offlinecontent.CourseTabViewData +import com.instructure.pandautils.features.offline.offlinecontent.OfflineItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +data class CourseTabViewModel( + val data: CourseTabViewData, + val courseId: Long, + val tabId: String, + @get:Bindable override var collapsed: Boolean, + val onCheckedChanged: (Boolean, CourseTabViewModel) -> Unit +) : GroupItemViewModel(collapsable = true, items = data.files) { + override val layoutId = R.layout.item_offline_tab + override val viewType = OfflineItemViewModelType.COURSE_TAB.viewType + + val onCheckChanged = CompoundButton.OnCheckedChangeListener { cb, checked -> + if (cb.isPressed) onCheckedChanged(checked, this) + } + + fun onRowClicked() { + data.synced = !data.synced + onCheckedChanged(data.synced, this) + } + + override fun areContentsTheSame(other: ItemViewModel): Boolean { + return other is CourseTabViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + && other.data == this.data + } + + override fun areItemsTheSame(other: ItemViewModel): Boolean { + return other is CourseTabViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/EmptyCourseContentViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/EmptyCourseContentViewModel.kt new file mode 100644 index 0000000000..58ca03e7a9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/EmptyCourseContentViewModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels + +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.offlinecontent.OfflineItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +class EmptyCourseContentViewModel : ItemViewModel { + override val layoutId = R.layout.item_offline_course_empty + override val viewType = OfflineItemViewModelType.EMPTY_COURSE_CONTENT.viewType +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/FileViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/FileViewModel.kt new file mode 100644 index 0000000000..d3b37231ab --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/offlinecontent/itemviewmodels/FileViewModel.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.features.offline.offlinecontent.itemviewmodels + +import android.widget.CompoundButton +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.offlinecontent.FileViewData +import com.instructure.pandautils.features.offline.offlinecontent.OfflineItemViewModelType +import com.instructure.pandautils.mvvm.ItemViewModel + +data class FileViewModel( + val data: FileViewData, + val courseId: Long, + val fileId: Long, + val fileUrl: String?, + val tabId: String, + val onCheckedChanged: (Boolean, FileViewModel) -> Unit +) : ItemViewModel { + override val layoutId = R.layout.item_offline_file + override val viewType = OfflineItemViewModelType.FILE.viewType + + val onCheckChanged = CompoundButton.OnCheckedChangeListener { cb, checked -> + if (cb.isPressed) onCheckedChanged(checked, this) + } + + fun onRowClicked() { + data.checked = !data.checked + onCheckedChanged(data.checked, this) + } + + override fun areContentsTheSame(other: ItemViewModel): Boolean { + return other is FileViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + && other.data == this.data + } + + override fun areItemsTheSame(other: ItemViewModel): Boolean { + return other is FileViewModel + && other.courseId == this.courseId + && other.tabId == this.tabId + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt new file mode 100644 index 0000000000..b343688602 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/AggregateProgressObserver.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.pandautils.R +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity + +class AggregateProgressObserver( + private val context: Context, + courseSyncProgressDao: CourseSyncProgressDao, + fileSyncProgressDao: FileSyncProgressDao +) { + + val progressData: LiveData + get() = _progressData + private val _progressData = MutableLiveData() + + private var courseProgressLiveData: LiveData>? = null + private var fileProgressLiveData: LiveData>? = null + + private var courseProgresses = mutableMapOf() + private var fileProgresses = mutableMapOf() + + private val courseProgressObserver = Observer> { + courseProgresses = it.associateBy { it.workerId }.toMutableMap() + + calculateProgress() + } + + private val fileProgressObserver = Observer> { + fileProgresses = it.associateBy { it.workerId }.toMutableMap() + + calculateProgress() + } + + init { + courseProgressLiveData = courseSyncProgressDao.findAllLiveData() + courseProgressLiveData?.observeForever(courseProgressObserver) + + fileProgressLiveData = fileSyncProgressDao.findAllLiveData() + fileProgressLiveData?.observeForever(fileProgressObserver) + } + + private fun calculateProgress() { + val courseProgresses = courseProgresses.values.toList() + val fileProgresses = fileProgresses.values.toList() + + if (courseProgresses.isEmpty() && fileProgresses.isEmpty()) { + _progressData.postValue(null) + return + } + + val totalSize = courseProgresses.sumOf { it.totalSize() } + fileProgresses.sumOf { it.fileSize } + val downloadedTabSize = courseProgresses.sumOf { it.downloadedSize() } + val downloadedFileSize = fileProgresses.sumOf { it.fileSize * (it.progress.toDouble() / 100.0) } + val downloadedSize = downloadedTabSize + downloadedFileSize.toLong() + val progress = (downloadedSize.toDouble() / totalSize.toDouble() * 100.0).toInt() + + val itemCount = courseProgresses.size + + val viewData = when { + courseProgresses.all { it.progressState == ProgressState.STARTING } -> { + AggregateProgressViewData( + title = context.getString(R.string.syncProgress_downloadStarting), + progressState = ProgressState.STARTING + ) + + } + + courseProgresses.all { it.progressState == ProgressState.COMPLETED } && fileProgresses.all { it.progressState == ProgressState.COMPLETED } -> { + val totalSizeString = NumberHelper.readableFileSize(context, totalSize) + AggregateProgressViewData( + progressState = ProgressState.COMPLETED, + title = context.getString(R.string.syncProgress_downloadSuccess, totalSizeString, totalSizeString), + progress = 100 + ) + + } + + fileProgresses.all { it.progressState.isFinished() } && courseProgresses.all { it.progressState.isFinished() } + && (courseProgresses.any { it.progressState == ProgressState.ERROR } || fileProgresses.any { it.progressState == ProgressState.ERROR }) -> { + AggregateProgressViewData( + progressState = ProgressState.ERROR, + title = context.getString(R.string.syncProgress_syncErrorSubtitle) + ) + + } + + else -> { + AggregateProgressViewData( + title = context.getString( + R.string.syncProgress_downloadProgress, + NumberHelper.readableFileSize(context, downloadedSize), + NumberHelper.readableFileSize(context, totalSize) + ), + totalSize = NumberHelper.readableFileSize(context, totalSize), + progress = progress, + itemCount = itemCount, + progressState = ProgressState.IN_PROGRESS + ) + } + } + + _progressData.postValue(viewData) + } + + fun onCleared() { + courseProgressLiveData?.removeObserver(courseProgressObserver) + fileProgressLiveData?.removeObserver(fileProgressObserver) + } +} + +data class AggregateProgressViewData( + val title: String, + val totalSize: String = "", + val progress: Int = 0, + val itemCount: Int = 0, + val progressState: ProgressState = ProgressState.STARTING +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSyncWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSyncWorker.kt new file mode 100644 index 0000000000..c3eeff2c97 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/CourseSyncWorker.kt @@ -0,0 +1,781 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync + +import android.content.Context +import android.net.Uri +import androidx.hilt.work.HiltWorker +import androidx.work.* +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.canvasapi2.apis.* +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.pandautils.features.offline.offlinecontent.CourseFileSharedRepository +import com.instructure.pandautils.room.offline.daos.* +import com.instructure.pandautils.room.offline.entities.* +import com.instructure.pandautils.room.offline.facade.* +import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.io.File + +@HiltWorker +class CourseSyncWorker @AssistedInject constructor( + @Assisted private val context: Context, + @Assisted private val workerParameters: WorkerParameters, + private val courseApi: CourseAPI.CoursesInterface, + private val pageApi: PageAPI.PagesInterface, + private val userApi: UserAPI.UsersInterface, + private val assignmentApi: AssignmentAPI.AssignmentInterface, + private val calendarEventApi: CalendarEventAPI.CalendarEventInterface, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val pageFacade: PageFacade, + private val userFacade: UserFacade, + private val courseFacade: CourseFacade, + private val assignmentFacade: AssignmentFacade, + private val quizDao: QuizDao, + private val quizApi: QuizAPI.QuizInterface, + private val scheduleItemFacade: ScheduleItemFacade, + private val conferencesApi: ConferencesApi.ConferencesInterface, + private val conferenceFacade: ConferenceFacade, + private val discussionApi: DiscussionAPI.DiscussionInterface, + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + private val announcementApi: AnnouncementAPI.AnnouncementInterface, + private val moduleApi: ModuleAPI.ModuleInterface, + private val moduleFacade: ModuleFacade, + private val featuresApi: FeaturesAPI.FeaturesInterface, + private val courseFeaturesDao: CourseFeaturesDao, + private val courseFileSharedRepository: CourseFileSharedRepository, + private val fileFolderDao: FileFolderDao, + private val fileSyncSettingsDao: FileSyncSettingsDao, + private val localFileDao: LocalFileDao, + private val workManager: WorkManager, + private val discussionTopicFacade: DiscussionTopicFacade, + private val groupApi: GroupAPI.GroupInterface, + private val groupFacade: GroupFacade, + private val syncSettingsFacade: SyncSettingsFacade, + private val enrollmentsApi: EnrollmentAPI.EnrollmentInterface, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao, + private val htmlParser: HtmlParser, + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface, + private val pageDao: PageDao, + private val firebaseCrashlytics: FirebaseCrashlytics +) : CoroutineWorker(context, workerParameters) { + + private lateinit var progress: CourseSyncProgressEntity + + private var fileOperation: Operation? = null + + private val additionalFileIdsToSync = mutableSetOf() + private val externalFilesToSync = mutableSetOf() + + override suspend fun doWork(): Result { + val courseSettingsWithFiles = + courseSyncSettingsDao.findWithFilesById(inputData.getLong(COURSE_ID, -1)) ?: return Result.failure() + val courseSettings = courseSettingsWithFiles.courseSyncSettings + val courseId = courseSettings.courseId + val course = fetchCourseDetails(courseId) + + progress = initProgress(courseSettings, course) + + if (courseSettings.fullFileSync || courseSettingsWithFiles.files.isNotEmpty()) { + fetchFiles(courseId) + } + + val workContinuation = syncFiles(courseSettings) + + if (courseSettings.isTabSelected(Tab.PAGES_ID)) { + fetchPages(courseId) + } else { + pageFacade.deleteAllByCourseId(courseId) + } + + // We need to do this after the pages request because we delete all the previous pages there + val isHomeTabAPage = Tab.FRONT_PAGE_ID == course.homePageID + if (isHomeTabAPage) { + fetchHomePage(courseId) + } + + if (courseSettings.areAnyTabsSelected(setOf(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID, Tab.SYLLABUS_ID))) { + fetchAssignments(courseId) + } else { + assignmentFacade.deleteAllByCourseId(courseId) + } + + if (courseSettings.isTabSelected(Tab.SYLLABUS_ID)) { + fetchSyllabus(courseId) + } else { + scheduleItemFacade.deleteAllByCourseId(courseId) + } + + if (courseSettings.isTabSelected(Tab.CONFERENCES_ID)) { + fetchConferences(courseId) + } else { + conferenceFacade.deleteAllByCourseId(courseId) + } + + if (courseSettings.isTabSelected(Tab.DISCUSSIONS_ID)) { + fetchDiscussions(courseId) + } else { + discussionTopicHeaderFacade.deleteAllByCourseId(courseId, false) + } + + if (courseSettings.isTabSelected(Tab.ANNOUNCEMENTS_ID)) { + fetchAnnouncements(courseId) + } else { + discussionTopicHeaderFacade.deleteAllByCourseId(courseId, true) + } + + if (courseSettings.isTabSelected(Tab.PEOPLE_ID)) { + fetchUsers(courseId) + } + + if (courseSettings.isTabSelected(Tab.QUIZZES_ID)) { + fetchAllQuizzes(CanvasContext.Type.COURSE.apiString, courseId) + } else if (!courseSettings.areAnyTabsSelected(setOf(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID, Tab.SYLLABUS_ID))) { + quizDao.deleteAllByCourseId(courseId) + } + + if (courseSettings.isTabSelected(Tab.MODULES_ID)) { + fetchModules(courseId, courseSettingsWithFiles) + } else { + moduleFacade.deleteAllByCourseId(courseId) + } + + syncAdditionalFiles(courseSettings, workContinuation) + + progress = + progress.copy(progressState = if (progress.tabs.any { it.value.state == ProgressState.ERROR }) ProgressState.ERROR else ProgressState.COMPLETED) + courseSyncProgressDao.update(progress) + + return Result.success() + } + + private suspend fun fetchSyllabus(courseId: Long) { + fetchTab(Tab.SYLLABUS_ID) { + val calendarEvents = fetchCalendarEvents(courseId) + val assignmentEvents = fetchCalendarAssignments(courseId) + val scheduleItems = mutableListOf() + + scheduleItems.addAll(calendarEvents) + scheduleItems.addAll(assignmentEvents) + + scheduleItemFacade.insertScheduleItems(scheduleItems, courseId) + } + } + + private suspend fun fetchCalendarEvents(courseId: Long): List { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val calendarEvents = calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.CALENDAR.apiName, + null, + null, + listOf("course_$courseId"), + restParams + ).depaginate { + calendarEventApi.next(it, restParams) + }.dataOrThrow + + calendarEvents.forEach { it.description = parseHtmlContent(it.description, courseId) } + + return calendarEvents + } + + private suspend fun fetchCalendarAssignments(courseId: Long): List { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val calendarAssignments = calendarEventApi.getCalendarEvents( + true, + CalendarEventAPI.CalendarEventType.ASSIGNMENT.apiName, + null, + null, + listOf("course_$courseId"), + restParams + ).depaginate { + calendarEventApi.next(it, restParams) + }.dataOrThrow + + calendarAssignments.forEach { it.description = parseHtmlContent(it.description, courseId) } + + return calendarAssignments + } + + private suspend fun fetchPages(courseId: Long) { + fetchTab(Tab.PAGES_ID) { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val pages = pageApi.getFirstPagePagesWithBody(courseId, CanvasContext.Type.COURSE.apiString, params) + .depaginate { nextUrl -> + pageApi.getNextPagePagesList(nextUrl, params) + }.dataOrThrow + + pages.forEach { + it.body = parseHtmlContent(it.body, courseId) + } + + pageFacade.insertPages(pages, courseId) + } + } + + private suspend fun fetchHomePage(courseId: Long) { + try { + val frontPage = pageApi.getFrontPage(CanvasContext.Type.COURSE.apiString, courseId, RestParams(isForceReadFromNetwork = true)).dataOrNull + if (frontPage != null) { + frontPage.body = parseHtmlContent(frontPage.body, courseId) + pageFacade.insertPage(frontPage, courseId) + } + } catch (e: Exception) { + firebaseCrashlytics.recordException(e) + } + } + + private suspend fun fetchAssignments(courseId: Long) { + fetchTab(Tab.ASSIGNMENTS_ID, Tab.GRADES_ID) { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val assignmentGroups = assignmentApi.getFirstPageAssignmentGroupListWithAssignments(courseId, restParams) + .depaginate { nextUrl -> + assignmentApi.getNextPageAssignmentGroupListWithAssignments(nextUrl, restParams) + }.dataOrThrow + + assignmentGroups.forEach { group -> + group.assignments.forEach { + it.description = parseHtmlContent(it.description, courseId) + it.discussionTopicHeader?.message = parseHtmlContent(it.discussionTopicHeader?.message, courseId) + } + } + + fetchQuizzes(assignmentGroups, courseId) + + assignmentFacade.insertAssignmentGroups(assignmentGroups, courseId) + } + } + + private suspend fun fetchCourseDetails(courseId: Long): Course { + val params = RestParams(isForceReadFromNetwork = true) + val course = courseApi.getFullCourseContent(courseId, params).dataOrThrow + val enrollments = course.enrollments.orEmpty().flatMap { + enrollmentsApi.getEnrollmentsForUserInCourse(courseId, it.userId, params).dataOrThrow + }.toMutableList() + + course.syllabusBody = parseHtmlContent(course.syllabusBody, courseId) + + courseFacade.insertCourse(course.copy(enrollments = enrollments)) + + val courseFeatures = featuresApi.getEnabledFeaturesForCourse(courseId, params).dataOrNull + courseFeatures?.let { + courseFeaturesDao.insert(CourseFeaturesEntity(courseId, it)) + } + + return course + } + + private suspend fun fetchUsers(courseId: Long) { + fetchTab(Tab.PEOPLE_ID) { + val restParams = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val users = userApi.getFirstPagePeopleList(courseId, CanvasContext.Type.COURSE.apiString, restParams) + .depaginate { userApi.getNextPagePeopleList(it, restParams) }.dataOrThrow + + userFacade.insertUsers(users, courseId) + } + } + + private suspend fun fetchQuizzes(assignmentGroups: List, courseId: Long) { + val params = RestParams(isForceReadFromNetwork = true) + val quizzes = mutableListOf() + assignmentGroups.forEach { group -> + group.assignments.forEach { assignment -> + if (assignment.quizId != 0L) { + val quiz = quizApi.getQuiz(assignment.courseId, assignment.quizId, params).dataOrNull + quiz?.description = parseHtmlContent(quiz?.description, courseId) + quiz?.let { quizzes.add(QuizEntity(it, assignment.courseId)) } + } + } + } + quizDao.deleteAndInsertAll(quizzes, courseId) + } + + private suspend fun fetchAllQuizzes(contextType: String, courseId: Long) { + fetchTab(Tab.QUIZZES_ID) { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val quizzes = quizApi.getFirstPageQuizzesList(contextType, courseId, params).depaginate { nextUrl -> + quizApi.getNextPageQuizzesList(nextUrl, params) + }.dataOrThrow + + quizzes.forEach { + it.description = parseHtmlContent(it.description, courseId) + } + + quizDao.deleteAndInsertAll(quizzes.map { QuizEntity(it, courseId) }, courseId) + } + } + + private suspend fun fetchConferences(courseId: Long) { + fetchTab(Tab.CONFERENCES_ID) { + val conferences = getConferencesForContext(CanvasContext.emptyCourseContext(courseId), true).dataOrThrow + + conferenceFacade.insertConferences(conferences, courseId) + } + } + + private suspend fun getConferencesForContext( + canvasContext: CanvasContext, forceNetwork: Boolean + ): DataResult> { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) + + return conferencesApi.getConferencesForContext(canvasContext.toAPIString().drop(1), params).map { + it.conferences + }.depaginate { url -> + conferencesApi.getNextPage(url, params).map { it.conferences } + } + } + + private suspend fun fetchDiscussions(courseId: Long) { + fetchTab(Tab.DISCUSSIONS_ID) { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val discussions = + discussionApi.getFirstPageDiscussionTopicHeaders(CanvasContext.Type.COURSE.apiString, courseId, params) + .depaginate { nextPage -> discussionApi.getNextPage(nextPage, params) }.dataOrThrow + + discussions.forEach { + it.message = parseHtmlContent(it.message, courseId) + } + + discussionTopicHeaderFacade.insertDiscussions(discussions, courseId, false) + + fetchDiscussionDetails(discussions, courseId) + } + } + + private suspend fun fetchAnnouncements(courseId: Long) { + fetchTab(Tab.ANNOUNCEMENTS_ID) { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val announcements = + announcementApi.getFirstPageAnnouncementsList(CanvasContext.Type.COURSE.apiString, courseId, params) + .depaginate { nextPage -> + announcementApi.getNextPageAnnouncementsList( + nextPage, + params + ) + }.dataOrThrow + + announcements.forEach { + it.message = parseHtmlContent(it.message, courseId) + } + + discussionTopicHeaderFacade.insertDiscussions(announcements, courseId, true) + } + } + + private suspend fun fetchDiscussionDetails(discussions: List, courseId: Long) { + val params = RestParams(isForceReadFromNetwork = true) + discussions.forEach { discussionTopicHeader -> + val discussionTopic = discussionApi.getFullDiscussionTopic(CanvasContext.Type.COURSE.apiString, courseId, discussionTopicHeader.id, 1, params).dataOrNull + discussionTopic?.let { + val topic = parseDiscussionTopicHtml(it, courseId) + discussionTopicFacade.insertDiscussionTopic(discussionTopicHeader.id, topic) + } + } + + val groups = groupApi.getFirstPageGroups(params).depaginate { nextUrl -> groupApi.getNextPageGroups(nextUrl, params) }.dataOrNull + + groups?.let { + it.forEach { group -> + ApiPrefs.user?.let { groupFacade.insertGroupWithUser(group, it) } + } + } + } + + private suspend fun parseDiscussionTopicHtml(discussionTopic: DiscussionTopic, courseId: Long): DiscussionTopic { + discussionTopic.views.map { parseHtmlContent(it.message, courseId) } + discussionTopic.views.map { it.replies?.map { parseDiscussionEntryHtml(it, courseId) } } + return discussionTopic + } + + private suspend fun parseDiscussionEntryHtml(discussionEntry: DiscussionEntry, courseId: Long): DiscussionEntry { + discussionEntry.message = parseHtmlContent(discussionEntry.message, courseId) + discussionEntry.replies?.map { parseDiscussionEntryHtml(it, courseId) } + return discussionEntry + } + + private suspend fun fetchModules(courseId: Long, courseSettings: CourseSyncSettingsWithFiles) { + fetchTab(Tab.MODULES_ID) { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + val moduleObjects = moduleApi.getFirstPageModuleObjects( + CanvasContext.Type.COURSE.apiString, courseId, params + ).depaginate { nextPage -> + moduleApi.getNextPageModuleObjectList(nextPage, params) + }.dataOrThrow.map { moduleObject -> + val moduleItems = moduleApi.getFirstPageModuleItems( + CanvasContext.Type.COURSE.apiString, + courseId, + moduleObject.id, + params + ).depaginate { nextPage -> + moduleApi.getNextPageModuleItemList(nextPage, params) + }.dataOrNull ?: moduleObject.items + moduleObject.copy(items = moduleItems) + } + + moduleFacade.insertModules(moduleObjects, courseId) + + val moduleItems = moduleObjects.flatMap { it.items } + moduleItems.forEach { + when (it.type) { + ModuleItem.Type.Page.name -> fetchPageModuleItem(courseId, it, params) + ModuleItem.Type.File.name -> fetchFileModuleItem(courseId, it, params, courseSettings) + ModuleItem.Type.Quiz.name -> fetchQuizModuleItem(courseId, it, params) + } + } + } + } + + private suspend fun fetchTab(vararg tabIds: String, fetchBlock: suspend () -> Unit) { + try { + fetchBlock() + updateTabSuccess(*tabIds) + } catch (e: Exception) { + updateTabError(*tabIds) + firebaseCrashlytics.recordException(e) + } + } + + private suspend fun fetchPageModuleItem( + courseId: Long, + it: ModuleItem, + params: RestParams + ) { + if (it.pageUrl != null && pageDao.findByUrl(it.pageUrl!!) == null) { + val page = pageApi.getDetailedPage(courseId, it.pageUrl!!, params).dataOrNull + page?.body = parseHtmlContent(page?.body, courseId) + page?.let { pageFacade.insertPage(it, courseId) } + } + } + + private suspend fun fetchFileModuleItem( + courseId: Long, + it: ModuleItem, + params: RestParams, + courseSettings: CourseSyncSettingsWithFiles + ) { + val fileId = it.contentId + if (courseSettings.files.any { it.id == fileId }) return // File is selected for sync so we don't need to sync it + + val file = fileFolderApi.getCourseFile(courseId, it.contentId, params).dataOrNull + if (file?.id != null) { + additionalFileIdsToSync.add(file.id) + } + } + + private suspend fun fetchQuizModuleItem( + courseId: Long, + it: ModuleItem, + params: RestParams + ) { + if (quizDao.findById(it.contentId) == null) { + val quiz = quizApi.getQuiz(courseId, it.contentId, params).dataOrNull + quiz?.description = parseHtmlContent(quiz?.description, courseId) + quiz?.let { quizDao.insert(QuizEntity(it, courseId)) } + } + } + + private suspend fun parseHtmlContent(htmlContent: String?, courseId: Long): String? { + val htmlParsingResult = htmlParser.createHtmlStringWithLocalFiles(htmlContent, courseId) + additionalFileIdsToSync.addAll(htmlParsingResult.internalFileIds) + externalFilesToSync.addAll(htmlParsingResult.externalFileUrls) + return htmlParsingResult.htmlWithLocalFileLinks + } + + private suspend fun syncFiles(syncSettings: CourseSyncSettingsEntity): WorkContinuation? { + val courseId = syncSettings.courseId + val allFiles = getAllFiles(courseId) + val allFileIds = allFiles.map { it.id } + + cleanupSyncedFiles(courseId, allFileIds) + + val fileSyncEntities = mutableListOf() + val fileWorkers = mutableListOf() + fileFolderDao.findFilesToSync(courseId, syncSettings.fullFileSync) + .forEach { + val worker = FileSyncWorker.createOneTimeWorkRequest( + courseId, + it.id, + it.displayName.orEmpty(), + it.url.orEmpty(), + syncSettingsFacade.getSyncSettings().wifiOnly + ) + fileWorkers.add(worker) + fileSyncEntities.add( + FileSyncProgressEntity( + workerId = worker.id.toString(), + courseId = courseId, + fileName = it.displayName.orEmpty(), + progress = 0, + fileSize = it.size, + progressState = ProgressState.STARTING, + fileId = it.id + ) + ) + } + + fileSyncProgressDao.insertAll(fileSyncEntities) + + val chunkedWorkers = fileWorkers.chunked(6) + + if (chunkedWorkers.isEmpty()) { + progress = progress.copy(progressState = ProgressState.IN_PROGRESS) + updateProgress() + + return null + } + + + var continuation = workManager + .beginWith(chunkedWorkers.first()) + + chunkedWorkers.drop(1).forEach { + continuation = continuation.then(it) + } + + fileOperation = continuation.enqueue() + + progress = progress.copy(progressState = ProgressState.IN_PROGRESS) + updateProgress() + + return continuation + } + + private suspend fun syncAdditionalFiles( + syncSettings: CourseSyncSettingsEntity, + workContinuation: WorkContinuation? + ) { + val courseId = syncSettings.courseId + + val fileSyncEntities = mutableListOf() + val fileWorkers = mutableListOf() + val additionalPublicFilesToSync = fileFolderDao.findByIds(additionalFileIdsToSync) + + additionalPublicFilesToSync.forEach { + val worker = FileSyncWorker.createOneTimeWorkRequest( + courseId, + it.id, + it.displayName.orEmpty(), + it.url.orEmpty(), + syncSettingsFacade.getSyncSettings().wifiOnly + ) + fileWorkers.add(worker) + fileSyncEntities.add( + FileSyncProgressEntity( + workerId = worker.id.toString(), + courseId = courseId, + fileName = it.displayName.orEmpty(), + progress = 0, + fileSize = it.size, + additionalFile = true, + progressState = ProgressState.STARTING, + fileId = it.id + ) + ) + } + + val nonPublicFileIds = additionalFileIdsToSync.minus(additionalPublicFilesToSync.map { it.id }.toSet()) + nonPublicFileIds.forEach { + val file = fileFolderApi.getCourseFile(courseId, it, RestParams(isForceReadFromNetwork = false)).dataOrNull + if (file != null) { + fileFolderDao.insert(FileFolderEntity(file)) + val worker = FileSyncWorker.createOneTimeWorkRequest( + courseId, + file.id, + file.displayName.orEmpty(), + file.url.orEmpty(), + syncSettingsFacade.getSyncSettings().wifiOnly + ) + fileWorkers.add(worker) + fileSyncEntities.add( + FileSyncProgressEntity( + workerId = worker.id.toString(), + courseId = courseId, + fileName = file.displayName.orEmpty(), + progress = 0, + fileSize = file.size, + additionalFile = true, + progressState = ProgressState.STARTING, + fileId = file.id + ) + ) + } + } + + externalFilesToSync.forEach { + val fileName = Uri.parse(it).lastPathSegment + if (fileName != null) { + val worker = FileSyncWorker.createOneTimeWorkRequest( + courseId, + -1, + fileName, + it, + syncSettingsFacade.getSyncSettings().wifiOnly + ) + fileWorkers.add(worker) + fileSyncEntities.add( + FileSyncProgressEntity( + workerId = worker.id.toString(), + courseId = courseId, + fileName = fileName, + progress = 0, + fileSize = 0, + additionalFile = true, + progressState = ProgressState.STARTING, + fileId = -1 + ) + ) + } + } + + fileSyncProgressDao.insertAll(fileSyncEntities) + + progress = progress.copy(additionalFilesStarted = true) + updateProgress() + + if (fileWorkers.isEmpty()) return + + val chunkedWorkers = fileWorkers.chunked(6) + + var continuation = + if (workContinuation != null) workContinuation.then(chunkedWorkers.first()) else workManager.beginWith( + chunkedWorkers.first() + ) + + chunkedWorkers.drop(1).forEach { + continuation = continuation.then(it) + } + + continuation.enqueue() + } + + private suspend fun fetchFiles(courseId: Long) { + val fileFolders = courseFileSharedRepository.getCourseFoldersAndFiles(courseId) + + val entities = fileFolders.map { FileFolderEntity(it) } + fileFolderDao.replaceAll(entities, courseId) + } + + private suspend fun cleanupSyncedFiles(courseId: Long, remoteIds: List) { + val syncedIds = fileSyncSettingsDao.findByCourseId(courseId).map { it.id } + val localRemovedFiles = localFileDao.findRemovedFiles(courseId, syncedIds) + val remoteRemovedFiles = localFileDao.findRemovedFiles(courseId, remoteIds) + + (localRemovedFiles + remoteRemovedFiles).forEach { + File(it.path).delete() + localFileDao.delete(it) + } + + val file = File(context.filesDir, "${ApiPrefs.user?.id.toString()}/external_$courseId") + file.listFiles()?.forEach { + it.delete() + } + + fileSyncSettingsDao.deleteAllExcept(courseId, remoteIds) + } + + private suspend fun getAllFiles(courseId: Long): List { + return fileFolderDao.findAllFilesByCourseId(courseId) + } + + private suspend fun updateProgress() { + courseSyncProgressDao.update(progress) + } + + private suspend fun initProgress( + courseSettings: CourseSyncSettingsEntity, + course: Course + ): CourseSyncProgressEntity { + val availableTabs = course.tabs?.map { it.tabId } ?: emptyList() + val selectedTabs = courseSettings.tabs.filter { availableTabs.contains(it.key) && it.value == true }.keys + val progress = (courseSyncProgressDao.findByCourseId(course.id) ?: createNewProgress(courseSettings)) + .copy( + tabs = selectedTabs.associateWith { tabId -> + TabSyncData( + course.tabs?.find { it.tabId == tabId }?.label ?: tabId, + ProgressState.IN_PROGRESS + ) + } + ) + + courseSyncProgressDao.update(progress) + return progress + } + + private suspend fun createNewProgress(courseSettings: CourseSyncSettingsEntity): CourseSyncProgressEntity { + val newProgress = CourseSyncProgressEntity( + workerId = workerParameters.id.toString(), + courseId = courseSettings.courseId, + courseName = courseSettings.courseName, + progressState = ProgressState.STARTING, + ) + courseSyncProgressDao.insert(newProgress) + return newProgress + } + + private suspend fun updateTabError(vararg tabIds: String) { + progress = progress.copy( + tabs = progress.tabs.toMutableMap().apply { + tabIds.forEach { tabId -> + val newProgress = get(tabId)?.copy(state = ProgressState.ERROR) ?: return@apply + put(tabId, newProgress) + } + + }, + ) + updateProgress() + } + + private suspend fun updateTabSuccess(vararg tabIds: String) { + progress = progress.copy( + tabs = progress.tabs.toMutableMap().apply { + tabIds.forEach { tabId -> + val newProgress = get(tabId)?.copy(state = ProgressState.COMPLETED) ?: return@apply + put(tabId, newProgress) + } + }, + ) + updateProgress() + } + + companion object { + const val COURSE_ID = "course_id" + const val TAG = "CourseSyncWorker" + + fun createOnTimeWork(courseId: Long, wifiOnly: Boolean): OneTimeWorkRequest { + val data = workDataOf(COURSE_ID to courseId) + return OneTimeWorkRequestBuilder() + .addTag(TAG) + .setInputData(data) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(if (wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + ) + .build() + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSyncWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSyncWorker.kt new file mode 100644 index 0000000000..480b41635f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/FileSyncWorker.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.instructure.canvasapi2.apis.DownloadState +import com.instructure.canvasapi2.apis.FileDownloadAPI +import com.instructure.canvasapi2.apis.saveFile +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import com.instructure.pandautils.utils.toJson +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.io.File +import java.util.Date + +@HiltWorker +class FileSyncWorker @AssistedInject constructor( + @Assisted private val context: Context, + @Assisted private val workerParameters: WorkerParameters, + private val fileDownloadApi: FileDownloadAPI, + private val localFileDao: LocalFileDao, + private val fileSyncProgressDao: FileSyncProgressDao, + private val firebaseCrashlytics: FirebaseCrashlytics +) : CoroutineWorker(context, workerParameters) { + + private var fileExists = false + + private lateinit var progress: FileSyncProgressEntity + + override suspend fun doWork(): Result { + val fileId = inputData.getLong(INPUT_FILE_ID, -1) + val inputFileName = inputData.getString(INPUT_FILE_NAME) ?: "" + val fileName = when { + inputFileName.isNotEmpty() && fileId != -1L -> "${fileId}_$inputFileName" + inputFileName.isNotEmpty() -> inputFileName + else -> fileId.toString() + } + val fileUrl = inputData.getString(INPUT_FILE_URL) ?: "" + val courseId = inputData.getLong(INPUT_COURSE_ID, -1) + + val externalFile = fileId == -1L + + var downloadedFile = getDownloadFile(fileName, externalFile, courseId) + + progress = fileSyncProgressDao.findByWorkerId(workerParameters.id.toString()) ?: return Result.failure() + + try { + fileDownloadApi.downloadFile(fileUrl, RestParams(shouldIgnoreToken = externalFile)) + .dataOrThrow + .saveFile(downloadedFile) + .collect { + when (it) { + is DownloadState.InProgress -> { + progress = progress.copy(progress = it.progress, progressState = ProgressState.IN_PROGRESS) + fileSyncProgressDao.update(progress) + } + + is DownloadState.Success -> { + if (fileExists) { + downloadedFile = rewriteOriginalFile(downloadedFile, fileName, externalFile, courseId) + } + if (!externalFile) { + localFileDao.insert(LocalFileEntity(fileId, courseId, Date(), downloadedFile.absolutePath)) + } + progress = progress.copy(progress = 100, progressState = ProgressState.COMPLETED, fileSize = it.totalBytes) + fileSyncProgressDao.update(progress) + } + + is DownloadState.Failure -> { + throw it.throwable + } + } + } + } catch (e: Exception) { + downloadedFile.delete() + progress = progress.copy(progressState = ProgressState.ERROR) + fileSyncProgressDao.update(progress) + firebaseCrashlytics.recordException(e) + } + + return Result.success() + } + + private fun getDownloadFile(fileName: String, externalFile: Boolean, courseId: Long): File { + var dir = File(context.filesDir, ApiPrefs.user?.id.toString()) + if (!dir.exists()) { + dir.mkdir() + } + + if (externalFile) { + dir = File(dir, "external_$courseId") + if (!dir.exists()) { + dir.mkdir() + } + } + + var downloadFile = File(dir, fileName) + if (downloadFile.exists()) { + downloadFile = File(dir, "temp_${fileName}") + fileExists = true + } + return downloadFile + } + + private fun rewriteOriginalFile(newFile: File, fileName: String, externalFile: Boolean, courseId: Long): File { + var dir = File(context.filesDir, ApiPrefs.user?.id.toString()) + if (externalFile) { + dir = File(dir, "external_$courseId") + } + val originalFile = File(dir, fileName) + originalFile.delete() + newFile.renameTo(originalFile) + return originalFile + } + + companion object { + const val INPUT_FILE_ID = "INPUT_FILE_ID" + const val INPUT_FILE_NAME = "INPUT_FILE_NAME" + const val INPUT_FILE_URL = "INPUT_FILE_URL" + const val INPUT_COURSE_ID = "INPUT_COURSE_ID" + const val PROGRESS = "fileSyncProgress" + const val OUTPUT = "fileSyncOutput" + const val TAG = "FileSyncWorker" + + fun createOneTimeWorkRequest(courseId: Long, fileId: Long, fileName: String, fileUrl: String, wifiOnly: Boolean): OneTimeWorkRequest { + val inputData = androidx.work.Data.Builder() + .putString(INPUT_FILE_NAME, fileName) + .putString(INPUT_FILE_URL, fileUrl) + .putLong(INPUT_FILE_ID, fileId) + .putLong(INPUT_COURSE_ID, courseId) + .build() + + return OneTimeWorkRequest.Builder(FileSyncWorker::class.java) + .addTag(TAG) + .setInputData(inputData) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(if (wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + ) + .build() + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt new file mode 100644 index 0000000000..f8d288eee7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/HtmlParser.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.features.offline.sync + +import android.content.Context +import android.net.Uri +import com.instructure.canvasapi2.apis.FileFolderAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File + +class HtmlParser( + private var localFileDao: LocalFileDao, + private val apiPrefs: ApiPrefs, + private val fileFolderDao: FileFolderDao, + @ApplicationContext private val context: Context, + private val fileSyncSettingsDao: FileSyncSettingsDao, + private val fileFolderApi: FileFolderAPI.FilesFoldersInterface +) { + + private val imageRegex = Regex("]*src=\"([^\"]*)\"[^>]*>") + private val fileLinkRegex = Regex("]*class=\"instructure_file_link[^>]*href=\"([^\"]*)\"[^>]*>") + private val internalFileRegex = Regex(".*${apiPrefs.domain}.*files/(\\d+)") + + suspend fun createHtmlStringWithLocalFiles(html: String?, courseId: Long): HtmlParsingResult { + if (html == null) return HtmlParsingResult(null, emptySet(), emptySet()) + + val imageParsingResult = parseAndReplaceImageTags(html, courseId) + val filesFromFileLinks = findFileIdsToSync(imageParsingResult.htmlWithLocalFileLinks ?: html) + + return imageParsingResult.copy(internalFileIds = imageParsingResult.internalFileIds + filesFromFileLinks) + } + + private suspend fun parseAndReplaceImageTags(originalHtml: String, courseId: Long): HtmlParsingResult { + var resultHtml: String = originalHtml + val internalFileIds = mutableSetOf() + val externalFileUrls = mutableSetOf() + + val matches = imageRegex.findAll(resultHtml) + matches.forEach { match -> + val imageUrl = match.groupValues[1] + val fileId = internalFileRegex.find(imageUrl)?.groupValues?.get(1)?.toLongOrNull() + if (fileId != null) { + val (newHtml, shouldSyncFile) = replaceInternalFileUrl(resultHtml, courseId, fileId, imageUrl) + resultHtml = newHtml + if (shouldSyncFile) internalFileIds.add(fileId) + } else { + val fileName = Uri.parse(imageUrl).lastPathSegment + if (fileName != null) { + resultHtml = resultHtml.replace(imageUrl, "file://${createLocalFilePathForExternalFile(fileName, courseId)}") + externalFileUrls.add(imageUrl) + } + } + } + + return HtmlParsingResult(resultHtml, internalFileIds, externalFileUrls) + } + + private suspend fun replaceInternalFileUrl(html: String, courseId: Long, fileId: Long, imageUrl: String): Pair { + var resultHtml = html + var shouldSyncFile = false + + val filePath = localFileDao.findById(fileId)?.path + if (!filePath.isNullOrEmpty()) { + resultHtml = resultHtml.replace(imageUrl, "file://$filePath") + } else { + resultHtml = resultHtml.replace(imageUrl, "file://${createLocalFilePath(fileId, courseId)}") + if (fileSyncSettingsDao.findById(fileId) == null) { + shouldSyncFile = true + } + } + + return Pair(resultHtml, shouldSyncFile) + } + + private suspend fun createLocalFilePath(fileId: Long, courseId: Long): String { + var fileName = fileFolderDao.findById(fileId)?.displayName.orEmpty() + if (fileName.isEmpty()) { + val file = fileFolderApi.getCourseFile(courseId, fileId, RestParams(isForceReadFromNetwork = false)).dataOrNull + fileName = file?.displayName.orEmpty() + } + val fileNameWithId = if (fileName.isNotEmpty()) "${fileId}_$fileName" else "$fileId" + val dir = File(context.filesDir, apiPrefs.user?.id.toString()) + + val downloadedFile = File(dir, fileNameWithId) + return downloadedFile.absolutePath + } + + private suspend fun createLocalFilePathForExternalFile(fileName: String, courseId: Long): String { + val dir = File(context.filesDir, "${apiPrefs.user?.id.toString()}/external_$courseId") + + val downloadedFile = File(dir, fileName) + return downloadedFile.absolutePath + } + + private suspend fun findFileIdsToSync(html: String): Set { + val internalFileIds = mutableSetOf() + + val fileMatches = fileLinkRegex.findAll(html) + fileMatches.forEach { match -> + val fileUrl = match.groupValues[1] + val fileId = internalFileRegex.find(fileUrl)?.groupValues?.get(1)?.toLongOrNull() + if (fileId != null) { + if (fileSyncSettingsDao.findById(fileId) == null) { + internalFileIds.add(fileId) + } + } + } + + return internalFileIds + } +} + +data class HtmlParsingResult( + val htmlWithLocalFileLinks: String?, + val internalFileIds: Set, + val externalFileUrls: Set +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt new file mode 100644 index 0000000000..432365e76b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncHelper.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.features.offline.sync + +import androidx.work.* +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import java.util.* +import java.util.concurrent.TimeUnit + +class OfflineSyncHelper( + private val workManager: WorkManager, + private val syncSettingsFacade: SyncSettingsFacade, + private val apiPrefs: ApiPrefs +) { + + suspend fun syncCourses(courseIds: List) { + if (isWorkScheduled() || !syncSettingsFacade.getSyncSettings().autoSyncEnabled) { + syncOnce(courseIds) + } else { + scheduleWork() + } + } + + fun cancelWork() { + workManager.cancelUniqueWork(apiPrefs.user?.id.toString()) + } + + suspend fun updateWork() { + val id = workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await().firstOrNull()?.id + val workRequest = createWorkRequest(id) + workManager.updateWork(workRequest) + } + + suspend fun scheduleWork() { + val workRequest = createWorkRequest() + workManager.enqueueUniquePeriodicWork( + apiPrefs.user?.id.toString(), + ExistingPeriodicWorkPolicy.UPDATE, + workRequest + ) + } + + fun syncOnce(courseIds: List) { + val inputData = Data.Builder() + .putLongArray(COURSE_IDS, courseIds.toLongArray()) + .build() + val workRequest = OneTimeWorkRequest.Builder(OfflineSyncWorker::class.java) + .setInputData(inputData) + .build() + workManager.enqueue(workRequest) + } + + private suspend fun isWorkScheduled(): Boolean { + return workManager.getWorkInfosForUniqueWork(apiPrefs.user?.id.toString()).await().isNotEmpty() + } + + private suspend fun createWorkRequest(id: UUID? = null): PeriodicWorkRequest { + val syncSettings = syncSettingsFacade.getSyncSettings() + val constraints = Constraints.Builder() + .setRequiredNetworkType(if (syncSettings.wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + val workRequestBuilder = PeriodicWorkRequest.Builder( + OfflineSyncWorker::class.java, + if (syncSettings.syncFrequency == SyncFrequency.DAILY) 1 else 7, TimeUnit.DAYS + ) + .setConstraints(constraints) + + id?.let { + workRequestBuilder.setId(it) + } + + return workRequestBuilder.build() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt new file mode 100644 index 0000000000..44c78fe445 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/OfflineSyncWorker.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.features.offline.sync + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.depaginate +import com.instructure.pandautils.R +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.DashboardCardDao +import com.instructure.pandautils.room.offline.daos.EditDashboardItemDao +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.DashboardCardEntity +import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity +import com.instructure.pandautils.room.offline.entities.EnrollmentState +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.io.File +import kotlin.random.Random + +const val COURSE_IDS = "course-ids" + +@HiltWorker +class OfflineSyncWorker @AssistedInject constructor( + @Assisted private val context: Context, + @Assisted workerParameters: WorkerParameters, + private val workManager: WorkManager, + private val featureFlagProvider: FeatureFlagProvider, + private val courseApi: CourseAPI.CoursesInterface, + private val dashboardCardDao: DashboardCardDao, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val syncSettingsFacade: SyncSettingsFacade, + private val editDashboardItemDao: EditDashboardItemDao, + private val courseDao: CourseDao, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao, + private val apiPrefs: ApiPrefs, + private val fileFolderDao: FileFolderDao, + private val localFileDao: LocalFileDao, + private val syncRouter: SyncRouter +) : CoroutineWorker(context, workerParameters) { + + private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val notificationId = Random.nextInt() + + override suspend fun doWork(): Result { + if (!featureFlagProvider.offlineEnabled()) return Result.success() + + val dashboardCards = + courseApi.getDashboardCourses(RestParams(isForceReadFromNetwork = true)).dataOrNull.orEmpty() + dashboardCardDao.updateEntities(dashboardCards.map { DashboardCardEntity(it) }) + + val params = RestParams(isForceReadFromNetwork = true, usePerPageQueryParam = true) + val currentCourses = courseApi.firstPageCoursesByEnrollmentState("active", params) + .depaginate { nextUrl -> courseApi.next(nextUrl, params) }.dataOrNull.orEmpty() + val pastCourses = courseApi.firstPageCoursesByEnrollmentState("completed", params) + .depaginate { nextUrl -> courseApi.next(nextUrl, params) }.dataOrNull.orEmpty() + val futureCourses = + courseApi.firstPageCoursesByEnrollmentState("invited_or_pending", params) + .depaginate { nextUrl -> courseApi.next(nextUrl, params) }.dataOrNull.orEmpty() + .filter { it.workflowState != Course.WorkflowState.UNPUBLISHED } + + val allCourses = currentCourses.mapIndexed { index, course -> + EditDashboardItemEntity( + course, + EnrollmentState.CURRENT, + index + ) + } + + pastCourses.mapIndexed { index, course -> + EditDashboardItemEntity( + course, + EnrollmentState.PAST, + index + ) + } + + futureCourses.mapIndexed { index, course -> + EditDashboardItemEntity( + course, + EnrollmentState.FUTURE, + index + ) + } + editDashboardItemDao.updateEntities(allCourses) + + val courseIds = inputData.getLongArray(COURSE_IDS) + val courses = courseIds?.let { + courseSyncSettingsDao.findByIds(courseIds.toList()) + } ?: courseSyncSettingsDao.findAll() + + val courseIdsToRemove = courseSyncSettingsDao.findAll().filter { !it.anySyncEnabled }.map { it.courseId } + courseDao.deleteByIds(courseIdsToRemove) + courseIdsToRemove.forEach { + cleanupFiles(it) + } + + val settingsMap = courses.associateBy { it.courseId } + + val courseWorkers = courses.filter { it.anySyncEnabled } + .map { CourseSyncWorker.createOnTimeWork(it.courseId, syncSettingsFacade.getSyncSettings().wifiOnly) } + + val courseProgresses = courseWorkers.map { + val courseId = it.workSpec.input.getLong(CourseSyncWorker.COURSE_ID, 0) + CourseSyncProgressEntity( + workerId = it.id.toString(), + courseId = courseId, + courseName = settingsMap[courseId]?.courseName.orEmpty(), + ) + } + + courseSyncProgressDao.deleteAll() + fileSyncProgressDao.deleteAll() + courseSyncProgressDao.insertAll(courseProgresses) + + workManager.beginWith(courseWorkers) + .enqueue() + + while (true) { + kotlinx.coroutines.delay(1000) + + val runningCourseProgresses = courseSyncProgressDao.findAll() + val runningFileProgresses = fileSyncProgressDao.findAll() + + if (runningCourseProgresses.all { it.progressState.isFinished() } && runningFileProgresses.all { it.progressState.isFinished() }) { + val itemCount = runningCourseProgresses.size + val isSuccess = + runningCourseProgresses.all { it.progressState == ProgressState.COMPLETED } && runningFileProgresses.all { it.progressState == ProgressState.COMPLETED } + showNotification(itemCount, isSuccess) + break + } + } + + return Result.success() + } + + private fun showNotification(itemCount: Int, success: Boolean) { + registerNotificationChannel(context) + + val pendingIntent = syncRouter.routeToSyncProgress(context) + + val title: String + val subtitle: String + if (success) { + title = context.getString(R.string.offlineContentSyncSuccessNotificationTitle) + subtitle = context.resources.getQuantityString( + R.plurals.offlineContentSyncSuccessNotificationBody, + itemCount, + itemCount + ) + } else { + title = context.getString(R.string.offlineContentSyncFailureNotificationTitle) + subtitle = context.getString(R.string.syncProgress_syncErrorSubtitle) + } + + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_canvas_logo) + .setContentTitle(title) + .setContentText(subtitle) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .build() + notificationManager.notify(notificationId, notification) + } + + private fun registerNotificationChannel(context: Context) { + if (notificationManager.notificationChannels.any { it.id == CHANNEL_ID }) return + + val name = context.getString(R.string.notificationChannelNameSyncUpdates) + val description = context.getString(R.string.notificationChannelNameSyncUpdatesDescription) + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel(CHANNEL_ID, name, importance) + channel.description = description + + notificationManager.createNotificationChannel(channel) + } + + private suspend fun cleanupFiles(courseId: Long) { + val file = File(context.filesDir, "${apiPrefs.user?.id.toString()}/external_$courseId") + file.deleteRecursively() + + fileFolderDao.deleteAllByCourseId(courseId) + localFileDao.findRemovedFiles(courseId, emptyList()).forEach { localFile -> + File(localFile.path).delete() + localFileDao.delete(localFile) + } + } + + companion object { + const val CHANNEL_ID = "syncChannel" + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/SyncData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/SyncData.kt new file mode 100644 index 0000000000..70fc669ec2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/SyncData.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync + +data class TabSyncData( + val tabName: String, + val state: ProgressState +) + +enum class ProgressState { + STARTING, + IN_PROGRESS, + COMPLETED, + ERROR; + + fun isFinished() = this == COMPLETED || this == ERROR + + fun isRunning() = this == IN_PROGRESS || this == STARTING +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/SyncRouter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/SyncRouter.kt new file mode 100644 index 0000000000..2b3a3733cc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/SyncRouter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync + +import android.app.PendingIntent +import android.content.Context + +interface SyncRouter { + + fun routeToSyncProgress(context: Context): PendingIntent +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressFragment.kt new file mode 100644 index 0000000000..581d8be60f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressFragment.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.instructure.interactions.router.Route +import com.instructure.pandautils.R +import com.instructure.pandautils.databinding.FragmentSyncProgressBinding +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.mvvm.ViewState +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.items +import com.instructure.pandautils.utils.setMenu +import com.instructure.pandautils.utils.setupAsBackButton +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SyncProgressFragment : Fragment() { + + private val viewModel: SyncProgressViewModel by viewModels() + + private lateinit var binding: FragmentSyncProgressBinding + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSyncProgressBinding.inflate(inflater, container, false) + binding.viewModel = viewModel + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + applyTheme() + + viewModel.progressData.observe(viewLifecycleOwner) { + when (it?.progressState) { + ProgressState.IN_PROGRESS -> { + setActionTitle(getString(R.string.cancel)) + } + + ProgressState.ERROR -> { + setActionTitle(getString(R.string.retry)) + } + + else -> { + setActionGone() + } + } + } + + viewModel.events.observe(viewLifecycleOwner) { + it.getContentIfNotHandled()?.let { + handleAction(it) + } + } + } + + private fun applyTheme() { + ViewStyler.themeToolbarColored( + requireActivity(), + binding.toolbar, + ThemePrefs.primaryColor, + ThemePrefs.primaryTextColor + ) + binding.toolbar.apply { + setBackgroundColor(ThemePrefs.primaryColor) + setupAsBackButton(this@SyncProgressFragment) + setMenu(R.menu.menu_sync_progress) { + viewModel.onActionClicked() + } + } + } + + private fun setActionTitle(title: String) { + binding.toolbar.menu.items.firstOrNull()?.isVisible = true + binding.toolbar.menu.items.firstOrNull()?.title = title + } + + private fun setActionGone() { + binding.toolbar.menu.items.firstOrNull()?.isVisible = false + } + + private fun handleAction(action: SyncProgressAction) { + when (action) { + is SyncProgressAction.CancelConfirmation -> { + showCancelConfirmation() + } + + is SyncProgressAction.Back -> { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + } + + private fun showCancelConfirmation() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.syncProgress_cancelConfirmationTitle) + .setMessage(R.string.syncProgress_cancelConfirmationMessage) + .setPositiveButton(R.string.yes) { _, _ -> + viewModel.cancel() + } + .setNegativeButton(R.string.no) { dialog, _ -> + dialog.dismiss() + } + .show() + } + + companion object { + fun newInstance() = SyncProgressFragment() + + fun makeRoute() = Route(SyncProgressFragment::class.java, null) + } + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt new file mode 100644 index 0000000000..756108e63a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewData.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress + +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import androidx.work.WorkInfo +import com.instructure.pandautils.BR +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.AdditionalFilesProgressItemViewModel +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.CourseProgressItemViewModel +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.FileSyncProgressItemViewModel +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.FilesTabProgressItemViewModel +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.TabProgressItemViewModel + +data class SyncProgressViewData(val items: List) + +data class CourseProgressViewData( + val courseName: String, + val courseId: Long, + val workerId: String, + val files: FilesTabProgressItemViewModel?, + val additionalFiles: AdditionalFilesProgressItemViewModel, + @Bindable var tabs: List? = null, + @Bindable var state: ProgressState = ProgressState.STARTING, + @Bindable var size: String = "", + @Bindable var failed: Boolean = false +) : BaseObservable() { + + fun updateState(newState: ProgressState) { + state = newState + notifyPropertyChanged(BR.state) + + if (state == ProgressState.ERROR) { + failed = true + notifyPropertyChanged(BR.failed) + } + } + + fun updateSize(size: String) { + this.size = size + notifyPropertyChanged(BR.size) + } +} + +data class TabProgressViewData( + val tabId: String, + val tabName: String, + val workerId: String, + @Bindable var state: ProgressState = ProgressState.IN_PROGRESS +) : BaseObservable() { + + fun updateState(newState: ProgressState) { + state = newState + notifyPropertyChanged(BR.state) + } +} + +data class FileSyncProgressViewData( + val fileName: String, + val fileSize: String, + val workerId: String, + @Bindable var progress: Int, + @Bindable var state: ProgressState = ProgressState.IN_PROGRESS +) : BaseObservable() { + + fun updateProgress(newProgress: Int) { + progress = newProgress + notifyPropertyChanged(BR.progress) + } + + fun updateState(newState: ProgressState) { + state = newState + notifyPropertyChanged(BR.state) + } +} + +data class FileTabProgressViewData( + val courseWorkerId: String, + var items: List, + @Bindable var totalSize: String = "", + @Bindable var progress: Int = 0, + @Bindable var state: ProgressState = ProgressState.IN_PROGRESS, + @Bindable var toggleable: Boolean = false +) : BaseObservable() { + + fun updateTotalSize(totalSize: String) { + this.totalSize = totalSize + notifyPropertyChanged(BR.totalSize) + } + + fun updateProgress(progress: Int) { + this.progress = progress + notifyPropertyChanged(BR.progress) + } +} + +data class AdditionalFilesProgressViewData( + val courseWorkerId: String, + @Bindable var totalSize: String = "", + @Bindable var state: ProgressState = ProgressState.IN_PROGRESS +) : BaseObservable() { + + fun updateTotalSize(totalSize: String) { + this.totalSize = totalSize + notifyPropertyChanged(BR.totalSize) + } +} + +enum class ViewType(val viewType: Int) { + COURSE_PROGRESS(0), + COURSE_TAB_PROGRESS(1), + COURSE_FILE_TAB_PROGRESS(2), + COURSE_FILE_PROGRESS(3), + COURSE_ADDITIONAL_FILES_PROGRESS(4), +} + +sealed class SyncProgressAction { + object CancelConfirmation : SyncProgressAction() + object Back : SyncProgressAction() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt new file mode 100644 index 0000000000..252e108f36 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/SyncProgressViewModel.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress + +import android.annotation.SuppressLint +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.work.WorkManager +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.sync.AggregateProgressObserver +import com.instructure.pandautils.features.offline.sync.AggregateProgressViewData +import com.instructure.pandautils.features.offline.sync.CourseSyncWorker +import com.instructure.pandautils.features.offline.sync.FileSyncWorker +import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.AdditionalFilesProgressItemViewModel +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.CourseProgressItemViewModel +import com.instructure.pandautils.features.offline.sync.progress.itemviewmodels.FilesTabProgressItemViewModel +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +@SuppressLint("StaticFieldLeak") +class SyncProgressViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val workManager: WorkManager, + private val courseSyncSettingsDao: CourseSyncSettingsDao, + private val offlineSyncHelper: OfflineSyncHelper, + private val aggregateProgressObserver: AggregateProgressObserver, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao +) : ViewModel() { + + val data: LiveData + get() = _data + private val _data = MutableLiveData() + + val progressData: LiveData + get() = aggregateProgressObserver.progressData + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + private val courseIds = mutableListOf() + + init { + viewModelScope.launch { + val courseSyncProgresses = courseSyncProgressDao.findAll() + if (courseSyncProgresses.isEmpty()) { + _events.postValue(Event(SyncProgressAction.Back)) + return@launch + } + courseIds.addAll(courseSyncProgresses.map { it.courseId }) + + val courses = courseSyncProgresses.map { + createCourseItem(it) + } + _data.postValue(SyncProgressViewData(courses)) + } + } + + private suspend fun createCourseItem(courseSyncProgressEntity: CourseSyncProgressEntity): CourseProgressItemViewModel { + val courseSyncSettings = courseSyncSettingsDao.findWithFilesById(courseSyncProgressEntity.courseId) + val data = CourseProgressViewData( + courseName = courseSyncProgressEntity.courseName, + courseId = courseSyncProgressEntity.courseId, + workerId = courseSyncProgressEntity.workerId, + size = context.getString(R.string.syncProgress_syncQueued), + files = if (courseSyncSettings?.files?.isNotEmpty() == true || courseSyncSettings?.courseSyncSettings?.fullFileSync == true) { + FilesTabProgressItemViewModel( + data = FileTabProgressViewData(courseWorkerId = courseSyncProgressEntity.workerId, items = emptyList()), + context = context, + courseSyncProgressDao = courseSyncProgressDao, + fileSyncProgressDao = fileSyncProgressDao + ) + } else { + null + }, + additionalFiles = + AdditionalFilesProgressItemViewModel( + data = AdditionalFilesProgressViewData(courseWorkerId = courseSyncProgressEntity.workerId), + fileSyncProgressDao = fileSyncProgressDao, + courseSyncProgressDao = courseSyncProgressDao, + context = context + ) + ) + + return CourseProgressItemViewModel(data, context, courseSyncProgressDao, fileSyncProgressDao) + } + + fun cancel() { + cancelRunningWorkers() + viewModelScope.launch { + courseSyncProgressDao.deleteAll() + fileSyncProgressDao.deleteAll() + } + _events.postValue(Event(SyncProgressAction.Back)) + } + + private fun cancelRunningWorkers() { + workManager.cancelAllWorkByTag(CourseSyncWorker.TAG) + workManager.cancelAllWorkByTag(FileSyncWorker.TAG) + } + + private fun retry() { + offlineSyncHelper.syncOnce(courseIds) + } + + fun onActionClicked() { + when (progressData.value?.progressState) { + ProgressState.ERROR -> { + viewModelScope.launch { + courseSyncProgressDao.deleteAll() + fileSyncProgressDao.deleteAll() + } + retry() + _events.postValue(Event(SyncProgressAction.Back)) + } + + ProgressState.IN_PROGRESS -> _events.postValue(Event(SyncProgressAction.CancelConfirmation)) + + else -> Unit + } + } + + override fun onCleared() { + super.onCleared() + aggregateProgressObserver.onCleared() + _data.value?.items?.forEach { it.onCleared() } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt new file mode 100644 index 0000000000..722ba63f68 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/AdditionalFilesProgressItemViewModel.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress.itemviewmodels + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.pandautils.BR +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.progress.AdditionalFilesProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.ViewType +import com.instructure.pandautils.mvvm.ItemViewModel +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity + +data class AdditionalFilesProgressItemViewModel( + val data: AdditionalFilesProgressViewData, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao, + private val context: Context +) : ItemViewModel { + override val layoutId = R.layout.item_additional_files_progress + + override val viewType = ViewType.COURSE_ADDITIONAL_FILES_PROGRESS.viewType + + private val totalFilesProgressObserver = Observer> { + when { + it.all { it.progressState == ProgressState.COMPLETED } -> { + data.state = ProgressState.COMPLETED + data.notifyPropertyChanged(BR.state) + } + + it.any { it.progressState == ProgressState.ERROR } -> { + data.state = ProgressState.ERROR + data.notifyPropertyChanged(BR.state) + } + } + + val totalSize = it.sumOf { it.fileSize } + + data.updateTotalSize(NumberHelper.readableFileSize(context, totalSize)) + } + + private val courseProgressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.courseWorkerId) + private var fileProgressLiveData: LiveData>? = null + + private val courseProgressObserver = Observer { progress -> + if (progress == null) return@Observer + if (!progress.additionalFilesStarted) return@Observer + + fileProgressLiveData = fileSyncProgressDao.findAdditionalFilesByCourseIdLiveData(progress.courseId) + fileProgressLiveData?.observeForever(totalFilesProgressObserver) + } + + init { + courseProgressLiveData.observeForever(courseProgressObserver) + } + + override fun onCleared() { + courseProgressLiveData.removeObserver(courseProgressObserver) + fileProgressLiveData?.removeObserver(totalFilesProgressObserver) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt new file mode 100644 index 0000000000..0325fa0071 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/CourseProgressItemViewModel.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress.itemviewmodels + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.work.WorkInfo +import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.pandautils.BR +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.TabSyncData +import com.instructure.pandautils.features.offline.sync.progress.CourseProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.TabProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.ViewType +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity + +data class CourseProgressItemViewModel( + val data: CourseProgressViewData, + private val context: Context, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao +) : + GroupItemViewModel(collapsable = true, items = emptyList(), collapsed = true) { + + override val layoutId: Int = R.layout.item_course_progress + + override val viewType: Int = ViewType.COURSE_PROGRESS.viewType + + private var courseProgressLiveData: LiveData? = null + private var fileProgressLiveData: LiveData>? = null + + private var courseSyncProgressEntity: CourseSyncProgressEntity? = null + private val fileProgresses = mutableMapOf() + + private val fileProgressObserver = Observer> { + it.forEach { fileProgress -> + fileProgresses[fileProgress.workerId] = fileProgress + } + + updateProgress() + } + + private val progressObserver = Observer { courseProgress -> + if (courseProgress == null) return@Observer + + courseSyncProgressEntity = courseProgress + + if (data.tabs == null && courseProgress.tabs.isNotEmpty()) { + createTabs(courseProgress.tabs, courseProgress.workerId) + } + + updateProgress() + } + + init { + courseProgressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.workerId) + courseProgressLiveData?.observeForever(progressObserver) + + fileProgressLiveData = fileSyncProgressDao.findByCourseIdLiveData(data.courseId) + fileProgressLiveData?.observeForever(fileProgressObserver) + } + + private fun createTabs(tabs: Map, courseWorkerId: String) { + val tabViewModels = tabs.map { tabEntry -> + TabProgressItemViewModel( + TabProgressViewData( + tabEntry.key, + tabEntry.value.tabName, + courseWorkerId, + tabEntry.value.state + ), + courseSyncProgressDao + ) + } + + data.tabs = tabViewModels + items = tabViewModels + listOf(data.files, data.additionalFiles).filterNotNull() + data.notifyPropertyChanged(BR.tabs) + } + + private fun updateProgress() { + val fileProgresses = fileProgresses.values + data.updateSize( + NumberHelper.readableFileSize( + context, + (courseSyncProgressEntity?.totalSize() ?: 0) + + fileProgresses.sumOf { it.fileSize }) + ) + + when { + courseSyncProgressEntity?.progressState == ProgressState.COMPLETED && fileProgresses.all { it.progressState == ProgressState.COMPLETED } -> { + data.updateState(ProgressState.COMPLETED) + } + + courseSyncProgressEntity?.progressState?.isFinished() == true && fileProgresses.all { it.progressState.isFinished() } + && (courseSyncProgressEntity?.progressState == ProgressState.ERROR) || fileProgresses.any { it.progressState == ProgressState.ERROR } -> { + data.updateState(ProgressState.ERROR) + } + + else -> { + data.updateState(ProgressState.IN_PROGRESS) + } + } + } + + private fun clearObservers() { + courseProgressLiveData?.removeObserver(progressObserver) + fileProgressLiveData?.removeObserver(fileProgressObserver) + } + + override fun onCleared() { + clearObservers() + data.tabs?.forEach { it.onCleared() } + data.files?.onCleared() + data.additionalFiles.onCleared() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FileSyncProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FileSyncProgressItemViewModel.kt new file mode 100644 index 0000000000..45ab76d1ed --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FileSyncProgressItemViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress.itemviewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.sync.progress.FileSyncProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.ViewType +import com.instructure.pandautils.mvvm.ItemViewModel +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity + +data class FileSyncProgressItemViewModel( + val data: FileSyncProgressViewData, + val fileSyncProgressDao: FileSyncProgressDao +) : ItemViewModel { + override val layoutId = R.layout.item_file_sync_progress + + override val viewType = ViewType.COURSE_FILE_PROGRESS.viewType + + private var fileSyncProgressLiveData: LiveData? = null + + private val fileSyncProgressObserver = Observer { progress -> + progress?.let { notifyChange(it) } + } + + init { + fileSyncProgressLiveData = fileSyncProgressDao.findByWorkerIdLiveData(data.workerId) + fileSyncProgressLiveData?.observeForever(fileSyncProgressObserver) + } + + private fun notifyChange(fileSyncProgress: FileSyncProgressEntity) { + data.updateProgress(fileSyncProgress.progress) + data.updateState(fileSyncProgress.progressState) + } + + override fun onCleared() { + fileSyncProgressLiveData?.removeObserver(fileSyncProgressObserver) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt new file mode 100644 index 0000000000..f3e26ab9a9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/FilesTabProgressItemViewModel.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress.itemviewmodels + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.pandautils.BR +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.GroupItemViewModel +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.progress.FileSyncProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.FileTabProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.ViewType +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity +import java.util.UUID + +data class FilesTabProgressItemViewModel( + val data: FileTabProgressViewData, + private val context: Context, + private val courseSyncProgressDao: CourseSyncProgressDao, + private val fileSyncProgressDao: FileSyncProgressDao +) : GroupItemViewModel(collapsable = true, items = data.items, collapsed = true) { + override val layoutId = R.layout.item_file_tab_progress + + override val viewType = ViewType.COURSE_FILE_TAB_PROGRESS.viewType + + private var fileProgressLiveData: LiveData>? = null + private val courseProgressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.courseWorkerId) + + private val fileProgressObserver = Observer> { progresses -> + if (progresses.isEmpty()) { + data.state = ProgressState.COMPLETED + data.notifyPropertyChanged(BR.state) + } else { + if (data.items.isEmpty()) { + createFileItems(progresses) + data.toggleable = true + data.notifyPropertyChanged(BR.toggleable) + } + + if (progresses.all { it.progressState.isFinished() }) { + when { + progresses.all { it.progressState == ProgressState.COMPLETED } -> { + data.state = ProgressState.COMPLETED + data.notifyPropertyChanged(BR.state) + } + + progresses.any { it.progressState == ProgressState.ERROR } -> { + data.state = ProgressState.ERROR + data.notifyPropertyChanged(BR.state) + } + } + } + + val totalProgress = progresses.sumOf { it.progress } + + data.updateProgress(totalProgress / progresses.size) + } + } + + private val courseProgressObserver = Observer { progress -> + if (progress == null) return@Observer + if (progress.progressState == ProgressState.STARTING) return@Observer + + fileProgressLiveData = fileSyncProgressDao.findCourseFilesByCourseIdLiveData(progress.courseId) + fileProgressLiveData?.observeForever(fileProgressObserver) + } + + init { + courseProgressLiveData.observeForever(courseProgressObserver) + } + + private fun createFileItems(fileSyncData: List) { + val fileItems = mutableListOf() + var totalSize = 0L + val workerIds = mutableListOf() + fileSyncData.forEach { + val item = FileSyncProgressItemViewModel( + data = FileSyncProgressViewData( + fileName = it.fileName, + fileSize = NumberHelper.readableFileSize(context, it.fileSize), + progress = 0, + workerId = it.workerId, + ), + fileSyncProgressDao = fileSyncProgressDao + ) + workerIds.add(UUID.fromString(it.workerId)) + fileItems.add(item) + totalSize += it.fileSize + } + + data.items = fileItems + items = data.items + data.updateTotalSize(NumberHelper.readableFileSize(context, totalSize)) + } + + override fun onCleared() { + courseProgressLiveData.removeObserver(courseProgressObserver) + fileProgressLiveData?.removeObserver(fileProgressObserver) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt new file mode 100644 index 0000000000..4f68a5ad9c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/progress/itemviewmodels/TabProgressItemViewModel.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.progress.itemviewmodels + +import androidx.lifecycle.Observer +import com.instructure.pandautils.R +import com.instructure.pandautils.features.offline.sync.progress.TabProgressViewData +import com.instructure.pandautils.features.offline.sync.progress.ViewType +import com.instructure.pandautils.mvvm.ItemViewModel +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity + +data class TabProgressItemViewModel( + val data: TabProgressViewData, + val courseSyncProgressDao: CourseSyncProgressDao +) : ItemViewModel { + override val layoutId = R.layout.item_tab_progress + + override val viewType = ViewType.COURSE_TAB_PROGRESS.viewType + + private val progressLiveData = courseSyncProgressDao.findByWorkerIdLiveData(data.workerId) + + private val progressObserver = Observer { progress -> + progress?.tabs?.get(data.tabId)?.let { tabProgress -> + data.updateState(tabProgress.state) + } + } + + init { + progressLiveData.observeForever(progressObserver) + } + + override fun onCleared() { + progressLiveData.removeObserver(progressObserver) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncFrequency.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncFrequency.kt new file mode 100644 index 0000000000..914c7a1fd8 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncFrequency.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.features.offline.sync.settings + +import androidx.annotation.StringRes +import com.instructure.pandautils.R + +enum class SyncFrequency(@StringRes val readable: Int) { + DAILY(R.string.daily), + + WEEKLY(R.string.weekly) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt new file mode 100644 index 0000000000..e6f79f7f18 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsFragment.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.instructure.interactions.FragmentInteractions +import com.instructure.interactions.Navigation +import com.instructure.pandautils.R +import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.databinding.FragmentSyncSettingsBinding +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.setupAsBackButton +import com.instructure.pandautils.utils.showThemed +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SyncSettingsFragment : Fragment(), FragmentInteractions { + + private val binding by viewBinding(FragmentSyncSettingsBinding::bind) + + private val viewModel: SyncSettingsViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_sync_settings, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + applyTheme() + + viewModel.events.observe(viewLifecycleOwner) { + it.getContentIfNotHandled()?.let { + handleAction(it) + } + } + } + + private fun handleAction(action: SyncSettingsAction) { + when (action) { + is SyncSettingsAction.ShowFrequencySelector -> showFrequencySelector( + action.items, + action.selectedItemPosition, + action.onItemSelected + ) + is SyncSettingsAction.ShowWifiConfirmation -> showWifiConfirmation(action.confirmationCallback) + } + } + + private fun showWifiConfirmation(confirmationCallback: (Boolean) -> Unit) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.syncSettings_wifiConfirmationTitle) + .setMessage(R.string.synySettings_wifiConfirmationMessage) + .setNegativeButton(R.string.cancel) { dialog, _ -> + confirmationCallback(false) + dialog.dismiss() + } + .setPositiveButton(R.string.syncSettings_wifiConfirmationPositiveButton) { dialog, _ -> + confirmationCallback(true) + dialog.dismiss() + } + .showThemed() + } + + private fun showFrequencySelector(items: List, selectedItemPosition: Int, onItemSelected: (Int) -> Unit) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.syncSettings_syncFrequencyDialogTitle) + .setSingleChoiceItems(items.toTypedArray(), selectedItemPosition) { dialog, selected -> + onItemSelected(selected) + dialog.dismiss() + } + .setNegativeButton(R.string.cancel) { dialog, _ -> + dialog.dismiss() + } + .showThemed() + } + + override val navigation: Navigation? + get() = activity as? Navigation + + override fun title(): String = getString(R.string.syncSettings_toolbarTitle) + + override fun applyTheme() { + ViewStyler.themeToolbarColored( + requireActivity(), + binding.toolbar, + ThemePrefs.primaryColor, + ThemePrefs.primaryTextColor + ) + + binding.toolbar.setupAsBackButton(this@SyncSettingsFragment) + } + + override fun getFragment(): Fragment = this + + companion object { + fun newInstance() = SyncSettingsFragment() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsViewData.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsViewData.kt new file mode 100644 index 0000000000..4c0e1e1d08 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsViewData.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.settings + +data class SyncSettingsViewData( + val autoSyncEnabled: Boolean, + val syncFrequency: String, + val wifiOnly: Boolean +) + +sealed class SyncSettingsAction { + data class ShowFrequencySelector( + val items: List, + val selectedItemPosition: Int, + val onItemSelected: (Int) -> Unit + ) : SyncSettingsAction() + + data class ShowWifiConfirmation( + val confirmationCallback: (Boolean) -> Unit + ) : SyncSettingsAction() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsViewModel.kt new file mode 100644 index 0000000000..654259bddc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/offline/sync/settings/SyncSettingsViewModel.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.features.offline.sync.settings + +import android.content.res.Resources +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.pandautils.features.offline.sync.OfflineSyncHelper +import com.instructure.pandautils.mvvm.Event +import com.instructure.pandautils.room.offline.entities.SyncSettingsEntity +import com.instructure.pandautils.room.offline.facade.SyncSettingsFacade +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SyncSettingsViewModel @Inject constructor( + private val syncSettingsFacade: SyncSettingsFacade, + private val offlineSyncHelper: OfflineSyncHelper, + private val resources: Resources +) : ViewModel() { + + val data: LiveData + get() = _data + private val _data = MutableLiveData() + + val events: LiveData> + get() = _events + private val _events = MutableLiveData>() + + private lateinit var syncSettings: SyncSettingsEntity + + init { + loadData() + } + + fun loadData() { + viewModelScope.launch { + syncSettings = syncSettingsFacade.getSyncSettings() + _data.postValue( + SyncSettingsViewData( + syncSettings.autoSyncEnabled, + resources.getString(syncSettings.syncFrequency.readable), + syncSettings.wifiOnly + ) + ) + } + } + + fun onAutoSyncChanged(checked: Boolean) { + viewModelScope.launch { + val updated = syncSettings.copy( + autoSyncEnabled = checked + ) + syncSettingsFacade.update(updated) + if (checked) { + offlineSyncHelper.scheduleWork() + } else { + offlineSyncHelper.cancelWork() + } + loadData() + } + } + + fun onWifiOnlyChanged(checked: Boolean) { + if (checked) { + updateWifiOnly(checked) + } else { + _events.postValue(Event(SyncSettingsAction.ShowWifiConfirmation { confirmed -> + if (confirmed) { + updateWifiOnly(checked) + } else { + loadData() + } + })) + } + } + + private fun updateWifiOnly(enabled: Boolean) { + viewModelScope.launch { + val updated = syncSettings.copy( + wifiOnly = enabled + ) + syncSettingsFacade.update(updated) + offlineSyncHelper.updateWork() + loadData() + } + } + + fun showFrequencySelector() { + val items = SyncFrequency.values() + val selectedItem = items.indexOf(syncSettings.syncFrequency) + + _events.postValue( + Event(SyncSettingsAction.ShowFrequencySelector( + items.map { resources.getString(it.readable) }, + selectedItem, + ) { + updateSyncFrequency(items[it]) + }) + ) + } + + private fun updateSyncFrequency(syncFrequency: SyncFrequency) { + viewModelScope.launch { + val updated = syncSettings.copy( + syncFrequency = syncFrequency + ) + syncSettingsFacade.update(updated) + offlineSyncHelper.updateWork() + loadData() + } + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/loaders/OpenMediaAsyncTaskLoader.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/loaders/OpenMediaAsyncTaskLoader.kt index 48a8d49acf..cc7eb9871e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/loaders/OpenMediaAsyncTaskLoader.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/loaders/OpenMediaAsyncTaskLoader.kt @@ -82,6 +82,7 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade private var mimeType: String? = null var url: String = "" + var path: String = "" var filename: String? = null private set private var isSubmission = false @@ -92,7 +93,9 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade init { if (args != null) { - url = args.getString(Const.URL) ?: throw IllegalArgumentException("Argument ${Const.URL} cannot be null") + path = args.getString(Const.PATH) ?: "" + url = args.getString(Const.URL) ?: "" + if (path.isBlank() && url.isBlank()) throw IllegalArgumentException("Both arguments ${Const.PATH} and ${Const.URL} cannot be null") isUseOutsideApps = args.getBoolean(Const.OPEN_OUTSIDE) if (args.containsKey(Const.MIME) && args.containsKey(Const.FILE_URL)) { mimeType = args.getString(Const.MIME) @@ -108,6 +111,7 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade if (args.containsKey(Const.EXTRAS)) { extras = args.getBundle(Const.EXTRAS) } + canvasContext = args.getParcelable(Const.CANVAS_CONTEXT) } } @@ -121,7 +125,7 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade val intent = Intent(Intent.ACTION_VIEW) intent.putExtra(Const.IS_MEDIA_TYPE, true) if (isHtmlFile && canvasContext != null) { - val file = downloadFile(context, url, filename) + val file = if (path.isNotBlank()) File(path) else downloadFile(context, url, filename) val bundle = FileUploadUtils.createTaskLoaderBundle( canvasContext, FileProvider.getUriForFile( @@ -141,7 +145,12 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade loadedMedia.setHtmlBundle(bundle) } else { loadedMedia.isHtmlFile = isHtmlFile - val uri = attemptConnection(url) + val uri = if (url.isNotBlank()) { + attemptConnection(url) + } else { + val file = File(path) + FileProvider.getUriForFile(context, context.applicationContext.packageName + Const.FILE_PROVIDER_AUTHORITY, file) + } if (uri != null) { intent.setDataAndType(uri, mimeType) loadedMedia.intent = intent @@ -235,7 +244,7 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade } private fun attemptDownloadFile(context: Context, intent: Intent, loadedMedia: LoadedMedia, url: String, filename: String?) { - val file = downloadFile(context, url, filename) + val file = if (path.isNotBlank()) File(path) else downloadFile(context, url, filename) val contentResolver = context.contentResolver val fileUri: Uri = FileProvider.getUriForFile( context, @@ -316,6 +325,16 @@ class OpenMediaAsyncTaskLoader(context: Context, args: Bundle?) : AsyncTaskLoade return openMediaBundle } + fun createLocalBundle(canvasContext: CanvasContext?, mime: String?, path: String?, filename: String?, useOutsideApps: Boolean): Bundle { + val openMediaBundle = Bundle() + openMediaBundle.putString(Const.MIME, mime) + openMediaBundle.putString(Const.PATH, path) + openMediaBundle.putString(Const.FILE_URL, filename) + openMediaBundle.putParcelable(Const.CANVAS_CONTEXT, canvasContext) + openMediaBundle.putBoolean(Const.OPEN_OUTSIDE, useOutsideApps) + return openMediaBundle + } + fun createBundle(canvasContext: CanvasContext?, mime: String?, url: String?, filename: String?, useOutsideApps: Boolean): Bundle { val openMediaBundle = createBundle(canvasContext, mime, url, filename) openMediaBundle.putBoolean(Const.OPEN_OUTSIDE, useOutsideApps) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ItemViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ItemViewModel.kt index 73d1a7f9f9..e95a67e8c0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ItemViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ItemViewModel.kt @@ -27,4 +27,6 @@ interface ItemViewModel { fun areItemsTheSame(other: ItemViewModel): Boolean = false fun areContentsTheSame(other: ItemViewModel): Boolean = false + + fun onCleared() = Unit } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ViewState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ViewState.kt index b902db2cbd..1a2bffa89d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ViewState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/mvvm/ViewState.kt @@ -17,10 +17,16 @@ package com.instructure.pandautils.mvvm import androidx.annotation.DrawableRes +import androidx.annotation.RawRes import androidx.annotation.StringRes sealed class ViewState { object Loading : ViewState() + data class LoadingWithAnimation( + @StringRes val titleRes: Int, + @StringRes val messageRes: Int, + @RawRes val animationRes: Int + ) : ViewState() object Success : ViewState() object Refresh : ViewState() object LoadingNextPage : ViewState() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/repository/Repository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/repository/Repository.kt new file mode 100644 index 0000000000..a89f9626ca --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/repository/Repository.kt @@ -0,0 +1,25 @@ +package com.instructure.pandautils.repository + +import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE +import com.instructure.pandautils.utils.FeatureFlagProvider +import com.instructure.pandautils.utils.NetworkStateProvider + +abstract class Repository( + private val localDataSource: T, + private val networkDataSource: T, + private val networkStateProvider: NetworkStateProvider, + private val featureFlagProvider: FeatureFlagProvider +) { + + fun isOnline() = networkStateProvider.isOnline() + + suspend fun isOfflineEnabled() = featureFlagProvider.offlineEnabled() + + suspend fun dataSource(): T { + return if (isOnline() || !isOfflineEnabled()) { + networkDataSource + } else { + localDataSource + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/MigrationUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/MigrationUtils.kt new file mode 100644 index 0000000000..73b60cf4e5 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/MigrationUtils.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +fun createMigration(from: Int, to: Int, migrationBlock: (SupportSQLiteDatabase) -> Unit): Migration { + return object : Migration(from, to) { + override fun migrate(database: SupportSQLiteDatabase) { + migrationBlock(database) + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt index 152bdae77c..29902a27b1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/AppDatabase.kt @@ -6,14 +6,6 @@ import androidx.room.TypeConverters import com.instructure.pandautils.room.appdatabase.daos.* import com.instructure.pandautils.room.appdatabase.entities.* import com.instructure.pandautils.room.common.Converters -import com.instructure.pandautils.room.common.daos.AttachmentDao -import com.instructure.pandautils.room.common.daos.AuthorDao -import com.instructure.pandautils.room.common.daos.MediaCommentDao -import com.instructure.pandautils.room.common.daos.SubmissionCommentDao -import com.instructure.pandautils.room.common.entities.AttachmentEntity -import com.instructure.pandautils.room.common.entities.AuthorEntity -import com.instructure.pandautils.room.common.entities.MediaCommentEntity -import com.instructure.pandautils.room.common.entities.SubmissionCommentEntity @Database( entities = [ diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDao.kt new file mode 100644 index 0000000000..ff244becfd --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/AttachmentDao.kt @@ -0,0 +1,26 @@ +package com.instructure.pandautils.room.appdatabase.daos + +import androidx.room.* +import com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity + +@Dao +interface AttachmentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(attachment: AttachmentEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(attachments: List) + + @Delete + suspend fun delete(attachment: AttachmentEntity) + + @Delete + suspend fun deleteAll(attachments: List) + + @Update + suspend fun update(attachment: AttachmentEntity) + + @Query("SELECT * FROM AttachmentEntity WHERE workerId=:parentId") + suspend fun findByParentId(parentId: String): List? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/AuthorDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/AuthorDao.kt new file mode 100644 index 0000000000..629f056120 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/AuthorDao.kt @@ -0,0 +1,17 @@ +package com.instructure.pandautils.room.appdatabase.daos + +import androidx.room.* +import com.instructure.pandautils.room.appdatabase.entities.AuthorEntity + +@Dao +interface AuthorDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(author: AuthorEntity) + + @Delete + suspend fun delete(author: AuthorEntity) + + @Update + suspend fun update(author: AuthorEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/MediaCommentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/MediaCommentDao.kt new file mode 100644 index 0000000000..2dc645a314 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/MediaCommentDao.kt @@ -0,0 +1,17 @@ +package com.instructure.pandautils.room.appdatabase.daos + +import androidx.room.* +import com.instructure.pandautils.room.appdatabase.entities.MediaCommentEntity + +@Dao +interface MediaCommentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(mediaComment: MediaCommentEntity) + + @Delete + suspend fun delete(mediaComment: MediaCommentEntity) + + @Update + suspend fun update(mediaComment: MediaCommentEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDao.kt new file mode 100644 index 0000000000..ef5313b76e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/daos/SubmissionCommentDao.kt @@ -0,0 +1,22 @@ +package com.instructure.pandautils.room.appdatabase.daos + +import androidx.room.* +import com.instructure.pandautils.room.appdatabase.entities.SubmissionCommentEntity +import com.instructure.pandautils.room.appdatabase.model.SubmissionCommentWithAttachments + +@Dao +interface SubmissionCommentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(submissionComment: SubmissionCommentEntity): Long + + @Delete + suspend fun delete(submissionComment: SubmissionCommentEntity) + + @Update + suspend fun update(submissionComment: SubmissionCommentEntity) + + @Transaction + @Query("SELECT * FROM SubmissionCommentEntity WHERE id=:id") + suspend fun findById(id: Long): SubmissionCommentWithAttachments? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/AttachmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/AttachmentEntity.kt similarity index 95% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/AttachmentEntity.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/AttachmentEntity.kt index 3c6c72e421..de457c7769 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/AttachmentEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/AttachmentEntity.kt @@ -1,9 +1,9 @@ -package com.instructure.pandautils.room.common.entities +package com.instructure.pandautils.room.appdatabase.entities import androidx.room.Entity import androidx.room.PrimaryKey import com.instructure.canvasapi2.models.Attachment -import java.util.Date +import java.util.* @Entity data class AttachmentEntity( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/AuthorEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/AuthorEntity.kt similarity index 91% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/AuthorEntity.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/AuthorEntity.kt index 43d0780318..81fbda4f72 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/AuthorEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/AuthorEntity.kt @@ -1,4 +1,4 @@ -package com.instructure.pandautils.room.common.entities +package com.instructure.pandautils.room.appdatabase.entities import androidx.room.Entity import androidx.room.PrimaryKey diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/MediaCommentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/MediaCommentEntity.kt similarity index 59% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/MediaCommentEntity.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/MediaCommentEntity.kt index 2a6de585ac..bb7cac2b42 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/MediaCommentEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/MediaCommentEntity.kt @@ -1,4 +1,4 @@ -package com.instructure.pandautils.room.common.entities +package com.instructure.pandautils.room.appdatabase.entities import androidx.room.Entity import androidx.room.PrimaryKey @@ -13,7 +13,7 @@ data class MediaCommentEntity( var mediaType: String? = null, var contentType: String? = null ) { - constructor(mediaComment: MediaComment): this( + constructor(mediaComment: MediaComment) : this( mediaComment.mediaId!!, mediaComment.displayName, mediaComment.url, @@ -21,13 +21,11 @@ data class MediaCommentEntity( mediaComment.contentType ) - fun toApiModel(): MediaComment { - return MediaComment( - mediaId, - displayName, - url, - mediaType?.let { MediaComment.MediaType.valueOf(it) }, - contentType - ) - } + fun toApiModel() = MediaComment( + mediaId, + displayName, + url, + mediaType?.let { MediaComment.MediaType.valueOf(it) }, + contentType + ) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/SubmissionCommentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/SubmissionCommentEntity.kt similarity index 92% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/SubmissionCommentEntity.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/SubmissionCommentEntity.kt index 7c17c84e09..db4660c434 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/entities/SubmissionCommentEntity.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/entities/SubmissionCommentEntity.kt @@ -1,9 +1,9 @@ -package com.instructure.pandautils.room.common.entities +package com.instructure.pandautils.room.appdatabase.entities import androidx.room.Entity import androidx.room.PrimaryKey import com.instructure.canvasapi2.models.SubmissionComment -import java.util.Date +import java.util.* @Entity data class SubmissionCommentEntity( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/model/SubmissionCommentWithAttachments.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/model/SubmissionCommentWithAttachments.kt new file mode 100644 index 0000000000..40bd4de657 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/appdatabase/model/SubmissionCommentWithAttachments.kt @@ -0,0 +1,44 @@ +package com.instructure.pandautils.room.appdatabase.model + +import androidx.room.Embedded +import androidx.room.Relation +import com.instructure.canvasapi2.models.SubmissionComment +import com.instructure.pandautils.room.appdatabase.entities.AttachmentEntity +import com.instructure.pandautils.room.appdatabase.entities.AuthorEntity +import com.instructure.pandautils.room.appdatabase.entities.MediaCommentEntity +import com.instructure.pandautils.room.appdatabase.entities.SubmissionCommentEntity + +data class SubmissionCommentWithAttachments( + @Embedded + val submissionComment: SubmissionCommentEntity, + @Relation( + parentColumn = "mediaCommentId", + entityColumn = "mediaId" + ) + val mediaComment: MediaCommentEntity?, + @Relation( + parentColumn = "id", + entityColumn = "submissionCommentId" + ) + val attachments: List?, + @Relation( + parentColumn = "authorId", + entityColumn = "id" + ) + val author: AuthorEntity? +) { + fun toApiModel(): SubmissionComment { + return SubmissionComment( + submissionComment.id, + submissionComment.authorId, + submissionComment.authorName, + submissionComment.authorPronouns, + submissionComment.comment, + submissionComment.createdAt, + mediaComment?.toApiModel(), + attachments?.let { it.map { it.toApiModel() } }?.let { ArrayList(it) } ?: ArrayList(), + author?.toApiModel(), + submissionComment.attemptId + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/Converters.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/Converters.kt index 9afca310b8..5d50ddc605 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/Converters.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/Converters.kt @@ -3,6 +3,7 @@ package com.instructure.pandautils.room.common import androidx.room.TypeConverter import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import com.instructure.canvasapi2.models.GradingSchemeRow import java.util.* class Converters { @@ -13,7 +14,7 @@ class Converters { @TypeConverter fun fromStringToListString(s: String): List { - return s.split(", ") + return s.takeIf { it.isNotEmpty() }?.split(", ").orEmpty() } @TypeConverter @@ -26,6 +27,16 @@ class Converters { return s.split(", ").mapNotNull { it.toLongOrNull() } } + @TypeConverter + fun fromLongArray(array: LongArray?) : String? { + return array?.joinToString() + } + + @TypeConverter + fun fromStringToLongArray(s: String?): LongArray? { + return s?.split(", ")?.mapNotNull { it.toLongOrNull() }?.toLongArray() + } + @TypeConverter fun dateToLong(date: Date?): Long? { return date?.time @@ -36,6 +47,16 @@ class Converters { return timestamp?.let { Date(it) } } + @TypeConverter + fun fromIntList(list: List?): String? { + return list?.joinToString(",") + } + + @TypeConverter + fun toIntList(s: String?): List? { + return s?.split(",")?.map { it.toInt() } + } + @TypeConverter fun stringToStringBooleanMap(value: String): Map { return Gson().fromJson(value, object : TypeToken>() {}.type) @@ -45,4 +66,14 @@ class Converters { fun stringBooleanMapToString(value: Map?): String { return if(value == null) "" else Gson().toJson(value) } + + @TypeConverter + fun gradingSchemeRowListToString(value: List?): String { + return if(value == null) "" else Gson().toJson(value) + } + + @TypeConverter + fun stringToGradingSchemeRowList(value: String): List? { + return Gson().fromJson(value, object : TypeToken>() {}.type) + } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/MigrationUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/MigrationUtils.kt index 4ae2fd7468..9b93e37edb 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/MigrationUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/MigrationUtils.kt @@ -19,6 +19,7 @@ package com.instructure.pandautils.room.common import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.instructure.pandautils.room.createMigration fun createMigration(from: Int, to: Int, migrationBlock: (SupportSQLiteDatabase) -> Unit): Migration { return object : Migration(from, to) { @@ -26,4 +27,4 @@ fun createMigration(from: Int, to: Int, migrationBlock: (SupportSQLiteDatabase) migrationBlock(database) } } -} \ No newline at end of file +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/MediaCommentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/MediaCommentDao.kt deleted file mode 100644 index f10912ce14..0000000000 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/MediaCommentDao.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.instructure.pandautils.room.common.daos - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Update -import com.instructure.pandautils.room.common.entities.MediaCommentEntity - -@Dao -interface MediaCommentDao { - - @Insert - suspend fun insert(mediaComment: MediaCommentEntity) - - @Delete - suspend fun delete(mediaComment: MediaCommentEntity) - - @Update - suspend fun update(mediaComment: MediaCommentEntity) - -} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/DatabaseProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/DatabaseProvider.kt new file mode 100644 index 0000000000..cf29d807b4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/DatabaseProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline + +interface DatabaseProvider { + + fun getDatabase(userId: Long?): OfflineDatabase + + fun clearDatabase(userId: Long) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineConverters.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineConverters.kt new file mode 100644 index 0000000000..06e8d4dfc0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineConverters.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline + +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.instructure.canvasapi2.models.GradingRule +import com.instructure.pandautils.features.offline.sync.TabSyncData +import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency +import com.instructure.pandautils.utils.toJson + +@TypeConverters +class OfflineConverters { + + @TypeConverter + fun fromGradingRule(gradingRule: GradingRule?): String? { + if (gradingRule == null) return null + return Gson().toJson(gradingRule) + } + + @TypeConverter + fun toGradingRule(s: String?): GradingRule? { + if (s == null) return null + return Gson().fromJson(s, GradingRule::class.java) + } + + @TypeConverter + fun fromSyncFrequency(syncFrequency: SyncFrequency): String { + return syncFrequency.name + } + + @TypeConverter + fun toSyncFrequency(string: String): SyncFrequency { + return SyncFrequency.valueOf(string) + } + + @TypeConverter + fun stringToTabSyncMap(value: String): Map { + return Gson().fromJson(value, object : TypeToken>() {}.type) + } + + @TypeConverter + fun tabSyncMapToString(value: Map): String { + return value.toJson() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt new file mode 100644 index 0000000000..0dd9d648e3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabase.kt @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.instructure.pandautils.room.common.Converters +import com.instructure.pandautils.room.offline.daos.AssignmentDao +import com.instructure.pandautils.room.offline.daos.AssignmentGroupDao +import com.instructure.pandautils.room.offline.daos.AssignmentOverrideDao +import com.instructure.pandautils.room.offline.daos.AssignmentRubricCriterionDao +import com.instructure.pandautils.room.offline.daos.AssignmentScoreStatisticsDao +import com.instructure.pandautils.room.offline.daos.AssignmentSetDao +import com.instructure.pandautils.room.offline.daos.AttachmentDao +import com.instructure.pandautils.room.offline.daos.AuthorDao +import com.instructure.pandautils.room.offline.daos.ConferenceDao +import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao +import com.instructure.pandautils.room.offline.daos.CourseDao +import com.instructure.pandautils.room.offline.daos.CourseFeaturesDao +import com.instructure.pandautils.room.offline.daos.CourseGradingPeriodDao +import com.instructure.pandautils.room.offline.daos.CourseSettingsDao +import com.instructure.pandautils.room.offline.daos.CourseSyncProgressDao +import com.instructure.pandautils.room.offline.daos.CourseSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.DashboardCardDao +import com.instructure.pandautils.room.offline.daos.DiscussionEntryDao +import com.instructure.pandautils.room.offline.daos.DiscussionParticipantDao +import com.instructure.pandautils.room.offline.daos.DiscussionTopicDao +import com.instructure.pandautils.room.offline.daos.DiscussionTopicHeaderDao +import com.instructure.pandautils.room.offline.daos.DiscussionTopicPermissionDao +import com.instructure.pandautils.room.offline.daos.EditDashboardItemDao +import com.instructure.pandautils.room.offline.daos.EnrollmentDao +import com.instructure.pandautils.room.offline.daos.FileFolderDao +import com.instructure.pandautils.room.offline.daos.FileSyncProgressDao +import com.instructure.pandautils.room.offline.daos.FileSyncSettingsDao +import com.instructure.pandautils.room.offline.daos.GradesDao +import com.instructure.pandautils.room.offline.daos.GradingPeriodDao +import com.instructure.pandautils.room.offline.daos.GroupDao +import com.instructure.pandautils.room.offline.daos.GroupUserDao +import com.instructure.pandautils.room.offline.daos.LocalFileDao +import com.instructure.pandautils.room.offline.daos.LockInfoDao +import com.instructure.pandautils.room.offline.daos.LockedModuleDao +import com.instructure.pandautils.room.offline.daos.MasteryPathAssignmentDao +import com.instructure.pandautils.room.offline.daos.MasteryPathDao +import com.instructure.pandautils.room.offline.daos.MediaCommentDao +import com.instructure.pandautils.room.offline.daos.ModuleCompletionRequirementDao +import com.instructure.pandautils.room.offline.daos.ModuleContentDetailsDao +import com.instructure.pandautils.room.offline.daos.ModuleItemDao +import com.instructure.pandautils.room.offline.daos.ModuleNameDao +import com.instructure.pandautils.room.offline.daos.ModuleObjectDao +import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.daos.PlannerOverrideDao +import com.instructure.pandautils.room.offline.daos.QuizDao +import com.instructure.pandautils.room.offline.daos.RubricCriterionAssessmentDao +import com.instructure.pandautils.room.offline.daos.RubricCriterionDao +import com.instructure.pandautils.room.offline.daos.RubricCriterionRatingDao +import com.instructure.pandautils.room.offline.daos.RubricSettingsDao +import com.instructure.pandautils.room.offline.daos.ScheduleItemAssignmentOverrideDao +import com.instructure.pandautils.room.offline.daos.ScheduleItemDao +import com.instructure.pandautils.room.offline.daos.SectionDao +import com.instructure.pandautils.room.offline.daos.SubmissionCommentDao +import com.instructure.pandautils.room.offline.daos.SubmissionDao +import com.instructure.pandautils.room.offline.daos.SyncSettingsDao +import com.instructure.pandautils.room.offline.daos.TabDao +import com.instructure.pandautils.room.offline.daos.TermDao +import com.instructure.pandautils.room.offline.daos.UserCalendarDao +import com.instructure.pandautils.room.offline.daos.UserDao +import com.instructure.pandautils.room.offline.entities.AssignmentDueDateEntity +import com.instructure.pandautils.room.offline.entities.AssignmentEntity +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity +import com.instructure.pandautils.room.offline.entities.AssignmentOverrideEntity +import com.instructure.pandautils.room.offline.entities.AssignmentRubricCriterionEntity +import com.instructure.pandautils.room.offline.entities.AssignmentScoreStatisticsEntity +import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity +import com.instructure.pandautils.room.offline.entities.AttachmentEntity +import com.instructure.pandautils.room.offline.entities.AuthorEntity +import com.instructure.pandautils.room.offline.entities.ConferenceEntity +import com.instructure.pandautils.room.offline.entities.ConferenceRecordingEntity +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity +import com.instructure.pandautils.room.offline.entities.CourseFilesEntity +import com.instructure.pandautils.room.offline.entities.CourseGradingPeriodEntity +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.DashboardCardEntity +import com.instructure.pandautils.room.offline.entities.DiscussionEntryAttachmentEntity +import com.instructure.pandautils.room.offline.entities.DiscussionEntryEntity +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicHeaderEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicPermissionEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicRemoteFileEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicSectionEntity +import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity +import com.instructure.pandautils.room.offline.entities.EnrollmentEntity +import com.instructure.pandautils.room.offline.entities.ExternalToolAttributesEntity +import com.instructure.pandautils.room.offline.entities.FileFolderEntity +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.GradesEntity +import com.instructure.pandautils.room.offline.entities.GradingPeriodEntity +import com.instructure.pandautils.room.offline.entities.GroupEntity +import com.instructure.pandautils.room.offline.entities.GroupUserEntity +import com.instructure.pandautils.room.offline.entities.LocalFileEntity +import com.instructure.pandautils.room.offline.entities.LockInfoEntity +import com.instructure.pandautils.room.offline.entities.LockedModuleEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathAssignmentEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathEntity +import com.instructure.pandautils.room.offline.entities.MediaCommentEntity +import com.instructure.pandautils.room.offline.entities.ModuleCompletionRequirementEntity +import com.instructure.pandautils.room.offline.entities.ModuleContentDetailsEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleNameEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity +import com.instructure.pandautils.room.offline.entities.NeedsGradingCountEntity +import com.instructure.pandautils.room.offline.entities.PageEntity +import com.instructure.pandautils.room.offline.entities.PlannerOverrideEntity +import com.instructure.pandautils.room.offline.entities.QuizEntity +import com.instructure.pandautils.room.offline.entities.RemoteFileEntity +import com.instructure.pandautils.room.offline.entities.RubricCriterionAssessmentEntity +import com.instructure.pandautils.room.offline.entities.RubricCriterionEntity +import com.instructure.pandautils.room.offline.entities.RubricCriterionRatingEntity +import com.instructure.pandautils.room.offline.entities.RubricSettingsEntity +import com.instructure.pandautils.room.offline.entities.ScheduleItemAssignmentOverrideEntity +import com.instructure.pandautils.room.offline.entities.ScheduleItemEntity +import com.instructure.pandautils.room.offline.entities.SectionEntity +import com.instructure.pandautils.room.offline.entities.SubmissionCommentEntity +import com.instructure.pandautils.room.offline.entities.SubmissionDiscussionEntryEntity +import com.instructure.pandautils.room.offline.entities.SubmissionEntity +import com.instructure.pandautils.room.offline.entities.SyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.TabEntity +import com.instructure.pandautils.room.offline.entities.TermEntity +import com.instructure.pandautils.room.offline.entities.UserCalendarEntity +import com.instructure.pandautils.room.offline.entities.UserEntity + +@Database( + entities = [ + AssignmentDueDateEntity::class, + AssignmentEntity::class, + AssignmentGroupEntity::class, + AssignmentOverrideEntity::class, + AssignmentRubricCriterionEntity::class, + AssignmentScoreStatisticsEntity::class, + AssignmentSetEntity::class, + CourseEntity::class, + CourseFilesEntity::class, + CourseGradingPeriodEntity::class, + CourseSettingsEntity::class, + CourseSyncSettingsEntity::class, + DashboardCardEntity::class, + DiscussionEntryAttachmentEntity::class, + DiscussionEntryEntity::class, + DiscussionParticipantEntity::class, + DiscussionTopicHeaderEntity::class, + DiscussionTopicPermissionEntity::class, + DiscussionTopicRemoteFileEntity::class, + DiscussionTopicSectionEntity::class, + EnrollmentEntity::class, + FileFolderEntity::class, + EditDashboardItemEntity::class, + ExternalToolAttributesEntity::class, + GradesEntity::class, + GradingPeriodEntity::class, + GroupEntity::class, + GroupUserEntity::class, + LocalFileEntity::class, + MasteryPathAssignmentEntity::class, + MasteryPathEntity::class, + ModuleContentDetailsEntity::class, + ModuleItemEntity::class, + ModuleObjectEntity::class, + NeedsGradingCountEntity::class, + PageEntity::class, + PlannerOverrideEntity::class, + RemoteFileEntity::class, + RubricCriterionAssessmentEntity::class, + RubricCriterionEntity::class, + RubricCriterionRatingEntity::class, + RubricSettingsEntity::class, + ScheduleItemAssignmentOverrideEntity::class, + ScheduleItemEntity::class, + SectionEntity::class, + SubmissionDiscussionEntryEntity::class, + SubmissionEntity::class, + SyncSettingsEntity::class, + TabEntity::class, + TermEntity::class, + UserCalendarEntity::class, + UserEntity::class, + QuizEntity::class, + LockInfoEntity::class, + LockedModuleEntity::class, + ModuleNameEntity::class, + ModuleCompletionRequirementEntity::class, + FileSyncSettingsEntity::class, + ConferenceEntity::class, + ConferenceRecordingEntity::class, + CourseFeaturesEntity::class, + AttachmentEntity::class, + MediaCommentEntity::class, + AuthorEntity::class, + SubmissionCommentEntity::class, + DiscussionTopicEntity::class, + CourseSyncProgressEntity::class, + FileSyncProgressEntity::class, + ], version = 1 +) +@TypeConverters(value = [Converters::class, OfflineConverters::class]) +abstract class OfflineDatabase : RoomDatabase() { + + abstract fun courseDao(): CourseDao + + abstract fun enrollmentDao(): EnrollmentDao + + abstract fun gradesDao(): GradesDao + + abstract fun gradingPeriodDao(): GradingPeriodDao + + abstract fun sectionDao(): SectionDao + + abstract fun termDao(): TermDao + + abstract fun userCalendarDao(): UserCalendarDao + + abstract fun userDao(): UserDao + + abstract fun courseGradingPeriodDao(): CourseGradingPeriodDao + + abstract fun tabDao(): TabDao + + abstract fun courseSyncSettingsDao(): CourseSyncSettingsDao + + abstract fun pageDao(): PageDao + + abstract fun assignmentGroupDao(): AssignmentGroupDao + + abstract fun assignmentDao(): AssignmentDao + + abstract fun rubricSettingsDao(): RubricSettingsDao + + abstract fun submissionDao(): SubmissionDao + + abstract fun groupDao(): GroupDao + + abstract fun plannerOverrideDao(): PlannerOverrideDao + + abstract fun discussionTopicHeaderDao(): DiscussionTopicHeaderDao + + abstract fun discussionParticipantDao(): DiscussionParticipantDao + + abstract fun syncSettingsDao(): SyncSettingsDao + + abstract fun assignmentScoreStatisticsDao(): AssignmentScoreStatisticsDao + + abstract fun rubricCriterionDao(): RubricCriterionDao + + abstract fun quizDao(): QuizDao + + abstract fun lockInfoDao(): LockInfoDao + + abstract fun lockedModuleDao(): LockedModuleDao + + abstract fun moduleNameDao(): ModuleNameDao + + abstract fun moduleCompletionRequirementDao(): ModuleCompletionRequirementDao + + abstract fun dashboardCardDao(): DashboardCardDao + + abstract fun fileSyncSettingsDao(): FileSyncSettingsDao + + abstract fun courseSettingsDao(): CourseSettingsDao + + abstract fun scheduleItemDao(): ScheduleItemDao + + abstract fun scheduleItemAssignmentOverrideDao(): ScheduleItemAssignmentOverrideDao + + abstract fun assignmentOverrideDao(): AssignmentOverrideDao + + abstract fun conferenceDao(): ConferenceDao + + abstract fun conferenceRecordingDao(): ConferenceRecodingDao + + abstract fun moduleObjectDao(): ModuleObjectDao + + abstract fun moduleItemDao(): ModuleItemDao + + abstract fun moduleContentDetailsDao(): ModuleContentDetailsDao + + abstract fun masteryPathDao(): MasteryPathDao + + abstract fun assignmentSetDao(): AssignmentSetDao + + abstract fun masteryPathAssignmentDao(): MasteryPathAssignmentDao + + abstract fun courseFeaturesDao(): CourseFeaturesDao + + abstract fun attachmentDao(): AttachmentDao + + abstract fun authorDao(): AuthorDao + + abstract fun mediaCommentDao(): MediaCommentDao + + abstract fun submissionCommentDao(): SubmissionCommentDao + + abstract fun rubricCriterionAssessmentDao(): RubricCriterionAssessmentDao + + abstract fun rubricCriterionRatingDao(): RubricCriterionRatingDao + + abstract fun assignmentRubricCriterionDao(): AssignmentRubricCriterionDao + + abstract fun fileFolderDao(): FileFolderDao + + abstract fun localFileDao(): LocalFileDao + + abstract fun editDashboardItemDao(): EditDashboardItemDao + + abstract fun courseSyncProgressDao(): CourseSyncProgressDao + + abstract fun fileSyncProgressDao(): FileSyncProgressDao + + abstract fun groupUserDao(): GroupUserDao + + abstract fun discussionTopicDao(): DiscussionTopicDao + + abstract fun discussionEntryDao(): DiscussionEntryDao + + abstract fun discussionTopicPermissionDao(): DiscussionTopicPermissionDao +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt new file mode 100644 index 0000000000..066f17bf82 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseMigrations.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline + +import androidx.room.migration.Migration +import com.instructure.pandautils.room.createMigration + +val offlineDatabaseMigrations = arrayOf() \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt new file mode 100644 index 0000000000..f429f63454 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/OfflineDatabaseProvider.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline + +import android.content.Context +import androidx.room.Room + +private const val OFFLINE_DB_PREFIX = "offline-db-" + +class OfflineDatabaseProvider(private val context: Context) : DatabaseProvider { + + private val dbMap = mutableMapOf() + + override fun getDatabase(userId: Long?): OfflineDatabase { + if (userId == null) throw IllegalStateException("You can't access the database while logged out") + + return dbMap.getOrPut(userId) { + Room.databaseBuilder(context, OfflineDatabase::class.java, "$OFFLINE_DB_PREFIX$userId") + .addMigrations(*offlineDatabaseMigrations) + .build() + } + } + + override fun clearDatabase(userId: Long) { + getDatabase(userId).clearAllTables() + dbMap.remove(userId) + context.deleteDatabase("$OFFLINE_DB_PREFIX$userId") + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentDao.kt new file mode 100644 index 0000000000..fc7c5ac62a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentDao.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.AssignmentEntity + +@Dao +interface AssignmentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: AssignmentEntity) + + @Upsert(entity = AssignmentEntity::class) + suspend fun insertOrUpdate(entity: AssignmentEntity) + + @Delete + suspend fun delete(entity: AssignmentEntity) + + @Update + suspend fun update(entity: AssignmentEntity) + + @Query("SELECT * FROM AssignmentEntity WHERE id = :id") + suspend fun findById(id: Long): AssignmentEntity? + + @Query("SELECT * FROM AssignmentEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentGroupDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentGroupDao.kt new file mode 100644 index 0000000000..79cef27828 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentGroupDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.AssignmentGroupEntity + +@Dao +interface AssignmentGroupDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: AssignmentGroupEntity) + + @Query("DELETE FROM AssignmentGroupEntity WHERE courseId=:id") + suspend fun deleteAllByCourseId(id: Long) + + @Delete + suspend fun delete(entity: AssignmentGroupEntity) + + @Update + suspend fun update(entity: AssignmentGroupEntity) + + @Query("SELECT * FROM AssignmentGroupEntity WHERE id = :id") + suspend fun findById(id: Long): AssignmentGroupEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentOverrideDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentOverrideDao.kt new file mode 100644 index 0000000000..8f7105e002 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentOverrideDao.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.AssignmentOverrideEntity + +@Dao +interface AssignmentOverrideDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: AssignmentOverrideEntity) + + @Delete + suspend fun delete(entity: AssignmentOverrideEntity) + + @Update + suspend fun update(entity: AssignmentOverrideEntity) + + @Query("SELECT * FROM AssignmentOverrideEntity WHERE id IN (:assignmentOverrideIds)") + suspend fun findByIds(assignmentOverrideIds: List): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentRubricCriterionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentRubricCriterionDao.kt new file mode 100644 index 0000000000..be18805a62 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentRubricCriterionDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.AssignmentRubricCriterionEntity + +@Dao +interface AssignmentRubricCriterionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: AssignmentRubricCriterionEntity) + + @Delete + suspend fun delete(entity: AssignmentRubricCriterionEntity) + + @Update + suspend fun update(entity: AssignmentRubricCriterionEntity) + + @Query("SELECT * FROM AssignmentRubricCriterionEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentScoreStatisticsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentScoreStatisticsDao.kt new file mode 100644 index 0000000000..f6b0623818 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentScoreStatisticsDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.AssignmentScoreStatisticsEntity + +@Dao +interface AssignmentScoreStatisticsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: AssignmentScoreStatisticsEntity) + + @Delete + suspend fun delete(entity: AssignmentScoreStatisticsEntity) + + @Update + suspend fun update(entity: AssignmentScoreStatisticsEntity) + + @Query("SELECT * FROM AssignmentScoreStatisticsEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): AssignmentScoreStatisticsEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentSetDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentSetDao.kt new file mode 100644 index 0000000000..55b26f6ad1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AssignmentSetDao.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity + +@Dao +interface AssignmentSetDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(assignmentSetEntity: AssignmentSetEntity) + + @Delete + suspend fun delete(assignmentSetEntity: AssignmentSetEntity) + + @Update + suspend fun update(assignmentSetEntity: AssignmentSetEntity) + + @Query("SELECT * FROM AssignmentSetEntity WHERE masteryPathId = :masteryPathId") + suspend fun findByMasteryPathId(masteryPathId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/AttachmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AttachmentDao.kt similarity index 86% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/AttachmentDao.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AttachmentDao.kt index 54b4c5d147..0ef7e87b2f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/AttachmentDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AttachmentDao.kt @@ -1,7 +1,7 @@ -package com.instructure.pandautils.room.common.daos +package com.instructure.pandautils.room.offline.daos import androidx.room.* -import com.instructure.pandautils.room.common.entities.AttachmentEntity +import com.instructure.pandautils.room.offline.entities.AttachmentEntity @Dao interface AttachmentDao { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/AuthorDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AuthorDao.kt similarity index 68% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/AuthorDao.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AuthorDao.kt index db2cd5ac8c..30470fd234 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/AuthorDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/AuthorDao.kt @@ -1,7 +1,7 @@ -package com.instructure.pandautils.room.common.daos +package com.instructure.pandautils.room.offline.daos import androidx.room.* -import com.instructure.pandautils.room.common.entities.AuthorEntity +import com.instructure.pandautils.room.offline.entities.AuthorEntity @Dao interface AuthorDao { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ConferenceDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ConferenceDao.kt new file mode 100644 index 0000000000..0cb189e1a4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ConferenceDao.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ConferenceEntity + +@Dao +interface ConferenceDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: ConferenceEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Delete + suspend fun delete(entity: ConferenceEntity) + + @Update + suspend fun update(entity: ConferenceEntity) + + @Query("SELECT * FROM ConferenceEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List + + @Query("DELETE FROM ConferenceEntity WHERE courseId = :courseId") + suspend fun deleteAllByCourseId(courseId: Long) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ConferenceRecodingDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ConferenceRecodingDao.kt new file mode 100644 index 0000000000..2991b4622b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ConferenceRecodingDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ConferenceRecordingEntity + +@Dao +interface ConferenceRecodingDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: ConferenceRecordingEntity) + + @Delete + suspend fun delete(entity: ConferenceRecordingEntity) + + @Update + suspend fun update(entity: ConferenceRecordingEntity) + + @Query("SELECT * FROM ConferenceRecordingEntity WHERE conferenceId = :conferenceId") + suspend fun findByConferenceId(conferenceId: Long): List +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseDao.kt new file mode 100644 index 0000000000..1402a5ff72 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseDao.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.CourseEntity + +@Dao +interface CourseDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CourseEntity) + + @Upsert(entity = CourseEntity::class) + suspend fun insertOrUpdate(entity: CourseEntity) + + @Delete + suspend fun delete(entity: CourseEntity) + + @Update + suspend fun update(entity: CourseEntity) + + @Query("SELECT * FROM CourseEntity WHERE id = :id") + suspend fun findById(id: Long): CourseEntity? + + @Query("SELECT * FROM CourseEntity WHERE id IN (:ids)") + suspend fun findByIds(ids: Set): List + + @Query("SELECT * FROM CourseEntity") + suspend fun findAll(): List + + @Query("DELETE FROM CourseEntity WHERE id IN (:ids)") + suspend fun deleteByIds(ids: List) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseFeaturesDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseFeaturesDao.kt new file mode 100644 index 0000000000..aedd46556d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseFeaturesDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.CourseFeaturesEntity + +@Dao +interface CourseFeaturesDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CourseFeaturesEntity) + + @Delete + suspend fun delete(entity: CourseFeaturesEntity) + + @Update + suspend fun update(entity: CourseFeaturesEntity) + + @Query("SELECT * FROM CourseFeaturesEntity WHERE id = :courseId") + suspend fun findByCourseId(courseId: Long): CourseFeaturesEntity? +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseGradingPeriodDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseGradingPeriodDao.kt new file mode 100644 index 0000000000..b32c65d64a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseGradingPeriodDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.CourseGradingPeriodEntity + +@Dao +interface CourseGradingPeriodDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CourseGradingPeriodEntity) + + @Delete + suspend fun delete(entity: CourseGradingPeriodEntity) + + @Update + suspend fun update(entity: CourseGradingPeriodEntity) + + @Query("SELECT * FROM CourseGradingPeriodEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSettingsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSettingsDao.kt new file mode 100644 index 0000000000..7417dd4fc4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSettingsDao.kt @@ -0,0 +1,20 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.CourseSettingsEntity + +@Dao +interface CourseSettingsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CourseSettingsEntity) + + @Delete + suspend fun delete(entity: CourseSettingsEntity) + + @Update + suspend fun update(entity: CourseSettingsEntity) + + @Query("SELECT * FROM CourseSettingsEntity WHERE courseId=:courseId") + suspend fun findByCourseId(courseId: Long): CourseSettingsEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt new file mode 100644 index 0000000000..a3ad581587 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncProgressDao.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.CourseSyncProgressEntity + +@Dao +interface CourseSyncProgressDao { + + @Insert + suspend fun insert(courseSyncProgressEntity: CourseSyncProgressEntity) + + @Insert + suspend fun insertAll(entities: List) + + @Update + suspend fun update(courseSyncProgressEntity: CourseSyncProgressEntity) + + @Query("SELECT * FROM CourseSyncProgressEntity") + suspend fun findAll(): List + + @Query("SELECT * FROM CourseSyncProgressEntity") + fun findAllLiveData(): LiveData> + + @Query("SELECT * FROM CourseSyncProgressEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): CourseSyncProgressEntity? + + @Query("SELECT * FROM CourseSyncProgressEntity WHERE workerId = :workerId") + fun findByWorkerIdLiveData(workerId: String): LiveData + + @Query("DELETE FROM CourseSyncProgressEntity") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncSettingsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncSettingsDao.kt new file mode 100644 index 0000000000..5a8fdca02c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/CourseSyncSettingsDao.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.model.CourseSyncSettingsWithFiles + +@Dao +interface CourseSyncSettingsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: CourseSyncSettingsEntity) + + @Delete + suspend fun delete(entity: CourseSyncSettingsEntity) + + @Update + suspend fun update(entity: CourseSyncSettingsEntity) + + @Query("SELECT * FROM CourseSyncSettingsEntity") + suspend fun findAll(): List + + @Query("SELECT * FROM CourseSyncSettingsEntity WHERE courseId=:courseId") + suspend fun findById(courseId: Long): CourseSyncSettingsEntity? + + @Query("SELECT * FROM CourseSyncSettingsEntity WHERE courseId IN (:courseIds)") + suspend fun findByIds(courseIds: List): List + + @Query("SELECT * FROM CourseSyncSettingsEntity WHERE courseId=:courseId") + suspend fun findWithFilesById(courseId: Long): CourseSyncSettingsWithFiles? + + @Query("SELECT * FROM CourseSyncSettingsEntity WHERE courseId IN (:courseIds)") + suspend fun findWithFilesByIds(courseIds: List): List + + @Query("SELECT * FROM CourseSyncSettingsEntity") + suspend fun findAllWithFiles(): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DashboardCardDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DashboardCardDao.kt new file mode 100644 index 0000000000..2aca42433b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DashboardCardDao.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.DashboardCardEntity + +@Dao +abstract class DashboardCardDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insertAll(entities: List) + + @Delete + abstract suspend fun delete(entity: DashboardCardEntity) + + @Update + abstract suspend fun update(entity: DashboardCardEntity) + + @Query("DELETE FROM DashboardCardEntity") + abstract suspend fun dropAll() + + @Query("SELECT * FROM DashboardCardEntity") + abstract suspend fun findAll(): List + + @Transaction + open suspend fun updateEntities(entities: List) { + dropAll() + insertAll(entities) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionEntryDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionEntryDao.kt new file mode 100644 index 0000000000..33891af341 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionEntryDao.kt @@ -0,0 +1,29 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.DiscussionEntryEntity + +@Dao +interface DiscussionEntryDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: DiscussionEntryEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List): List + + @Delete + suspend fun delete(entity: DiscussionEntryEntity) + + @Update + suspend fun update(entity: DiscussionEntryEntity) + + @Query("SELECT * FROM DiscussionEntryEntity WHERE id = :id") + suspend fun findById(id: Long): DiscussionEntryEntity? + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionParticipantDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionParticipantDao.kt new file mode 100644 index 0000000000..23b688058e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionParticipantDao.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity + +@Dao +interface DiscussionParticipantDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: DiscussionParticipantEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List): List + + @Upsert + suspend fun upsertAll(entities: List): List + + @Delete + suspend fun delete(entity: DiscussionParticipantEntity) + + @Update + suspend fun update(entity: DiscussionParticipantEntity) + + @Query("SELECT * FROM DiscussionParticipantEntity WHERE id = :id") + suspend fun findById(id: Long): DiscussionParticipantEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicDao.kt new file mode 100644 index 0000000000..232d61a317 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicDao.kt @@ -0,0 +1,29 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.DiscussionTopicEntity + +@Dao +interface DiscussionTopicDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: DiscussionTopicEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List): List + + @Delete + suspend fun delete(entity: DiscussionTopicEntity) + + @Update + suspend fun update(entity: DiscussionTopicEntity) + + @Query("SELECT * FROM DiscussionTopicEntity WHERE id = :id") + suspend fun findById(id: Long): DiscussionTopicEntity? + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicHeaderDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicHeaderDao.kt new file mode 100644 index 0000000000..f43819358d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicHeaderDao.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.DiscussionTopicHeaderEntity + +@Dao +interface DiscussionTopicHeaderDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: DiscussionTopicHeaderEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List): List + + @Delete + suspend fun delete(entity: DiscussionTopicHeaderEntity) + + @Update + suspend fun update(entity: DiscussionTopicHeaderEntity) + + @Query("SELECT * FROM DiscussionTopicHeaderEntity WHERE id = :id") + suspend fun findById(id: Long): DiscussionTopicHeaderEntity? + + @Query("SELECT * FROM DiscussionTopicHeaderEntity WHERE announcement = 0 AND courseId = :courseId") + suspend fun findAllDiscussionsForCourse(courseId: Long): List + + @Query("SELECT * FROM DiscussionTopicHeaderEntity WHERE announcement = 1 AND courseId = :courseId ORDER BY postedDate DESC") + suspend fun findAllAnnouncementsForCourse(courseId: Long): List + + @Query("DELETE FROM DiscussionTopicHeaderEntity WHERE courseId = :courseId AND announcement = :isAnnouncement") + suspend fun deleteAllByCourseId(courseId: Long, isAnnouncement: Boolean) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicPermissionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicPermissionDao.kt new file mode 100644 index 0000000000..1d67853c76 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/DiscussionTopicPermissionDao.kt @@ -0,0 +1,27 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.DiscussionTopicPermissionEntity + +@Dao +interface DiscussionTopicPermissionDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: DiscussionTopicPermissionEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List): List + + @Delete + suspend fun delete(entity: DiscussionTopicPermissionEntity) + + @Update + suspend fun update(entity: DiscussionTopicPermissionEntity) + + @Query("SELECT * FROM DiscussionTopicPermissionEntity WHERE discussionTopicHeaderId = :id") + suspend fun findByDiscussionTopicHeaderId(id: Long): DiscussionTopicPermissionEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/EditDashboardItemDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/EditDashboardItemDao.kt new file mode 100644 index 0000000000..dcaa232544 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/EditDashboardItemDao.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.EditDashboardItemEntity +import com.instructure.pandautils.room.offline.entities.EnrollmentState + +@Dao +interface EditDashboardItemDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insertAll(entities: List) + + @Delete + abstract suspend fun delete(entity: EditDashboardItemEntity) + + @Update + abstract suspend fun update(entity: EditDashboardItemEntity) + + @Query("DELETE FROM EditDashboardItemEntity") + abstract suspend fun dropAll() + + @Query("SELECT * FROM EditDashboardItemEntity WHERE enrollmentState = :enrollmentState ORDER BY position") + abstract suspend fun findByEnrollmentState(enrollmentState: EnrollmentState): List + + @Transaction + open suspend fun updateEntities(entities: List) { + dropAll() + insertAll(entities) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/EnrollmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/EnrollmentDao.kt new file mode 100644 index 0000000000..5a03c3e827 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/EnrollmentDao.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.EnrollmentEntity + +@Dao +interface EnrollmentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: EnrollmentEntity): Long + + @Upsert(entity = EnrollmentEntity::class) + suspend fun insertOrUpdate(entity: EnrollmentEntity) + + @Delete + suspend fun delete(entity: EnrollmentEntity) + + @Update + suspend fun update(entity: EnrollmentEntity) + + @Query("SELECT * FROM EnrollmentEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List + + @Query("SELECT * FROM EnrollmentEntity") + suspend fun findAll(): List + + @Query("SELECT * FROM EnrollmentEntity WHERE currentGradingPeriodId = :gradingPeriodId") + suspend fun findByGradingPeriodId(gradingPeriodId: Long): List + + @Query("SELECT * FROM EnrollmentEntity WHERE courseId = :courseId AND role = :role") + suspend fun findByCourseIdAndRole(courseId: Long, role: String): List + + @Query("SELECT * FROM EnrollmentEntity WHERE userId = :userId") + suspend fun findByUserId(userId: Long): EnrollmentEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt new file mode 100644 index 0000000000..8950d6ef61 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileFolderDao.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.FileFolderEntity + +@Dao +abstract class FileFolderDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insert(fileFolder: FileFolderEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insertAll(fileFolders: List) + + @Query("DELETE FROM FileFolderEntity WHERE folderId in (SELECT id FROM FileFolderEntity WHERE contextId = :courseId) OR contextId = :courseId") + abstract suspend fun deleteAllByCourseId(courseId: Long) + + @Update + abstract suspend fun update(fileFolder: FileFolderEntity) + + @Query("SELECT * FROM FileFolderEntity WHERE folderId in (SELECT id FROM FileFolderEntity WHERE contextId = :courseId)") + abstract suspend fun findAllFilesByCourseId(courseId: Long): List + + @Query("SELECT * FROM FileFolderEntity WHERE id = :id") + abstract suspend fun findById(id: Long): FileFolderEntity? + + @Query("SELECT * FROM FileFolderEntity WHERE id IN (:ids)") + abstract suspend fun findByIds(ids: Set): List + + @Query("SELECT * FROM FileFolderEntity WHERE parentFolderId = :parentId AND isHidden = 0 AND isHiddenForUser = 0") + abstract suspend fun findVisibleFoldersByParentId(parentId: Long): List + + @Query("SELECT * FROM FileFolderEntity WHERE folderId = :folderId AND isHidden = 0 AND isHiddenForUser = 0") + abstract suspend fun findVisibleFilesByFolderId(folderId: Long): List + + @Query("SELECT * FROM FileFolderEntity WHERE contextId = :contextId AND parentFolderId = 0") + abstract suspend fun findRootFolderForContext(contextId: Long): FileFolderEntity? + + @Transaction + open suspend fun replaceAll(fileFolders: List, courseId: Long) { + deleteAllByCourseId(courseId) + insertAll(fileFolders) + } + + @Query("SELECT * FROM FileFolderEntity WHERE folderId in (SELECT id FROM FileFolderEntity WHERE contextId = :courseId)" + + " AND (updatedDate > IFNULL((SELECT createdDate FROM LocalFileEntity WHERE id = FileFolderEntity.id), 0) OR createdDate > IFNULL((SELECT createdDate FROM LocalFileEntity WHERE id = FileFolderEntity.id), 0))") + abstract suspend fun findFilesToSyncFull(courseId: Long): List + + @Query("SELECT * FROM FileFolderEntity WHERE folderId in (SELECT id FROM FileFolderEntity WHERE contextId = :courseId)" + + " AND (updatedDate > IFNULL((SELECT createdDate FROM LocalFileEntity WHERE id = FileFolderEntity.id), 0) OR createdDate > IFNULL((SELECT createdDate FROM LocalFileEntity WHERE id = FileFolderEntity.id), 0))" + + " AND id in (SELECT id FROM FileSyncSettingsEntity WHERE courseId = :courseId)") + abstract suspend fun findFilesToSyncSelected(courseId: Long): List + + suspend fun findFilesToSync(courseId: Long, fullSync: Boolean): List { + return if (fullSync) { + findFilesToSyncFull(courseId) + } else { + findFilesToSyncSelected(courseId) + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt new file mode 100644 index 0000000000..adbb09b6cb --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncProgressDao.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.FileSyncProgressEntity + +@Dao +interface FileSyncProgressDao { + + @Insert + suspend fun insert(fileSyncProgressEntity: FileSyncProgressEntity) + + @Insert + suspend fun insertAll(fileSyncProgressEntities: List) + + @Update + suspend fun update(fileSyncProgressEntity: FileSyncProgressEntity) + + @Query("SELECT * FROM FileSyncProgressEntity WHERE workerId = :workerId") + suspend fun findByWorkerId(workerId: String): FileSyncProgressEntity? + + @Query("SELECT * FROM FileSyncProgressEntity WHERE workerId = :workerId") + fun findByWorkerIdLiveData(workerId: String): LiveData + + @Query("SELECT * FROM FileSyncProgressEntity WHERE courseId = :courseId") + fun findByCourseIdLiveData(courseId: Long): LiveData> + + @Query("SELECT * FROM FileSyncProgressEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List + + @Query("SELECT * FROM FileSyncProgressEntity WHERE fileId = :fileId") + suspend fun findByFileId(fileId: Long): FileSyncProgressEntity? + + @Query("SELECT * FROM FileSyncProgressEntity WHERE additionalFile = 1 AND courseId = :courseId") + fun findAdditionalFilesByCourseIdLiveData(courseId: Long): LiveData> + + @Query("SELECT * FROM FileSyncProgressEntity WHERE additionalFile = 0 AND courseId = :courseId") + fun findCourseFilesByCourseIdLiveData(courseId: Long): LiveData> + + @Query("SELECT * FROM FileSyncProgressEntity") + fun findAllLiveData(): LiveData> + + @Query("SELECT * FROM FileSyncProgressEntity") + suspend fun findAll(): List + + @Query("DELETE FROM FileSyncProgressEntity") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncSettingsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncSettingsDao.kt new file mode 100644 index 0000000000..fd85eb9fa8 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/FileSyncSettingsDao.kt @@ -0,0 +1,50 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity + +@Dao +abstract class FileSyncSettingsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insert(entity: FileSyncSettingsEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insertAll(entities: List) + + @Delete + abstract suspend fun delete(entity: FileSyncSettingsEntity) + + @Update + abstract suspend fun update(entity: FileSyncSettingsEntity) + + @Query("SELECT * FROM FileSyncSettingsEntity") + abstract suspend fun findAll(): List + + @Query("SELECT * FROM FileSyncSettingsEntity WHERE id=:id") + abstract suspend fun findById(id: Long): FileSyncSettingsEntity? + + @Query("DELETE FROM FileSyncSettingsEntity WHERE id=:fileId") + abstract suspend fun deleteById(fileId: Long) + + @Query("DELETE FROM FileSyncSettingsEntity WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List) + + @Query("DELETE FROM FileSyncSettingsEntity WHERE courseId=:courseId") + abstract suspend fun deleteByCourseId(courseId: Long) + + @Query("SELECT * FROM FileSyncSettingsEntity WHERE courseId=:courseId") + abstract suspend fun findByCourseId(courseId: Long): List + + @Transaction + open suspend fun updateCourseFiles(courseId: Long, fileSyncSettings: List) { + deleteByCourseId(courseId) + insertAll(fileSyncSettings) + } + + @Query("DELETE FROM FileSyncSettingsEntity WHERE courseId = :courseId AND id NOT IN (:ids)") + abstract suspend fun deleteAllExcept(courseId: Long, ids: List) + + @Query("SELECT * FROM FileSyncSettingsEntity WHERE courseId = :courseId AND id NOT IN (:ids)") + abstract suspend fun findComplements(courseId: Long, ids: List): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GradesDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GradesDao.kt new file mode 100644 index 0000000000..06f4549490 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GradesDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.GradesEntity + +@Dao +interface GradesDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: GradesEntity) + + @Delete + suspend fun delete(entity: GradesEntity) + + @Update + suspend fun update(entity: GradesEntity) + + @Query("SELECT * FROM GradesEntity WHERE enrollmentId = :enrollmentId") + suspend fun findByEnrollmentId(enrollmentId: Long): GradesEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDao.kt new file mode 100644 index 0000000000..a7ad1fb331 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GradingPeriodDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.GradingPeriodEntity + +@Dao +interface GradingPeriodDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(entity: GradingPeriodEntity) + + @Delete + suspend fun delete(entity: GradingPeriodEntity) + + @Update + suspend fun update(entity: GradingPeriodEntity) + + @Query("SELECT * FROM GradingPeriodEntity WHERE id = :id") + suspend fun findById(id: Long): GradingPeriodEntity +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GroupDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GroupDao.kt new file mode 100644 index 0000000000..77c99ce7e1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GroupDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.GroupEntity + +@Dao +interface GroupDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: GroupEntity): Long + + @Upsert + suspend fun insertOrUpdate(entity: GroupEntity) + + @Delete + suspend fun delete(entity: GroupEntity) + + @Update + suspend fun update(entity: GroupEntity) + + @Query("SELECT * FROM GroupEntity WHERE id = :id") + suspend fun findById(id: Long): GroupEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GroupUserDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GroupUserDao.kt new file mode 100644 index 0000000000..1cf73d132f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/GroupUserDao.kt @@ -0,0 +1,29 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.GroupUserEntity + +@Dao +interface GroupUserDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: GroupUserEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List): List + + @Delete + suspend fun delete(entity: GroupUserEntity) + + @Update + suspend fun update(entity: GroupUserEntity) + + @Query("SELECT t.groupId FROM GroupUserEntity t WHERE t.userId = :userId") + suspend fun findByUserId(userId: Long): List? + +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LocalFileDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LocalFileDao.kt new file mode 100644 index 0000000000..df9f986d64 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LocalFileDao.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.LocalFileEntity + +@Dao +interface LocalFileDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(localFile: LocalFileEntity) + + @Update + suspend fun update(localFile: LocalFileEntity) + + @Delete + suspend fun delete(localFile: LocalFileEntity) + + @Query("SELECT * FROM LocalFileEntity WHERE id = :id") + suspend fun findById(id: Long): LocalFileEntity? + + @Query("SELECT * FROM LocalFileEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List + + @Query("SELECT EXISTS(SELECT * FROM LocalFileEntity WHERE id = :id)") + suspend fun existsById(id: Long): Boolean + + @Query("SELECT * FROM LocalFileEntity WHERE id IN (:ids)") + suspend fun findByIds(ids: List): List + + @Query("SELECT * FROM LocalFileEntity WHERE courseId = :courseId AND id NOT IN (:ids)") + suspend fun findRemovedFiles(courseId: Long, ids: List): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt new file mode 100644 index 0000000000..7da1353da9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockInfoDao.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.LockInfoEntity + +@Dao +interface LockInfoDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: LockInfoEntity): Long + + @Delete + suspend fun delete(entity: LockInfoEntity) + + @Update + suspend fun update(entity: LockInfoEntity) + + @Query("SELECT * FROM LockInfoEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): LockInfoEntity? + + @Query("SELECT * FROM LockInfoEntity WHERE moduleId = :moduleId") + suspend fun findByModuleId(moduleId: Long): LockInfoEntity? + + @Query("SELECT * FROM LockInfoEntity WHERE pageId = :pageId") + suspend fun findByPageId(pageId: Long): LockInfoEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockedModuleDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockedModuleDao.kt new file mode 100644 index 0000000000..4a33db5d54 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/LockedModuleDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.LockedModuleEntity + +@Dao +interface LockedModuleDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: LockedModuleEntity) + + @Delete + suspend fun delete(entity: LockedModuleEntity) + + @Update + suspend fun update(entity: LockedModuleEntity) + + @Query("SELECT * FROM LockedModuleEntity WHERE id = :id") + suspend fun findById(id: Long): LockedModuleEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MasteryPathAssignmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MasteryPathAssignmentDao.kt new file mode 100644 index 0000000000..3004371b0c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MasteryPathAssignmentDao.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.MasteryPathAssignmentEntity + +@Dao +interface MasteryPathAssignmentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(masteryPathAssignment: MasteryPathAssignmentEntity) + + @Delete + suspend fun delete(masteryPathAssignment: MasteryPathAssignmentEntity) + + @Update + suspend fun update(masteryPathAssignment: MasteryPathAssignmentEntity) + + @Query("SELECT * FROM MasteryPathAssignmentEntity WHERE assignmentSetId = :assignmentSetId") + suspend fun findByAssignmentSetId(assignmentSetId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MasteryPathDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MasteryPathDao.kt new file mode 100644 index 0000000000..fca4c873b7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MasteryPathDao.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.MasteryPathEntity + +@Dao +interface MasteryPathDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(masteryPath: MasteryPathEntity) + + @Delete + suspend fun delete(masteryPath: MasteryPathEntity) + + @Update + suspend fun update(masteryPath: MasteryPathEntity) + + @Query("SELECT * FROM MasteryPathEntity WHERE id = :id") + suspend fun findById(id: Long): MasteryPathEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MediaCommentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MediaCommentDao.kt new file mode 100644 index 0000000000..94c12e894a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/MediaCommentDao.kt @@ -0,0 +1,20 @@ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.MediaCommentEntity + +@Dao +interface MediaCommentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(mediaComment: MediaCommentEntity) + + @Delete + suspend fun delete(mediaComment: MediaCommentEntity) + + @Update + suspend fun update(mediaComment: MediaCommentEntity) + + @Query("SELECT * FROM MediaCommentEntity WHERE mediaId = :id") + suspend fun findById(id: String?): MediaCommentEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDao.kt new file mode 100644 index 0000000000..7b2fbf9898 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleCompletionRequirementDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ModuleCompletionRequirementEntity + +@Dao +interface ModuleCompletionRequirementDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: ModuleCompletionRequirementEntity) + + @Delete + suspend fun delete(entity: ModuleCompletionRequirementEntity) + + @Update + suspend fun update(entity: ModuleCompletionRequirementEntity) + + @Query("SELECT * FROM ModuleCompletionRequirementEntity WHERE moduleId = :moduleId") + suspend fun findByModuleId(moduleId: Long): List + + @Query("SELECT * FROM ModuleCompletionRequirementEntity WHERE id = :id") + suspend fun findById(id: Long): ModuleCompletionRequirementEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleContentDetailsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleContentDetailsDao.kt new file mode 100644 index 0000000000..ec8d7f384b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleContentDetailsDao.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.ModuleContentDetailsEntity + +@Dao +interface ModuleContentDetailsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(moduleContentDetails: ModuleContentDetailsEntity) + + @Delete + suspend fun delete(moduleContentDetails: ModuleContentDetailsEntity) + + @Update + suspend fun update(moduleContentDetails: ModuleContentDetailsEntity) + + @Query("SELECT * FROM ModuleContentDetailsEntity WHERE id = :id") + suspend fun findById(id: Long): ModuleContentDetailsEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleItemDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleItemDao.kt new file mode 100644 index 0000000000..815a2b05a4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleItemDao.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity + +@Dao +interface ModuleItemDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(moduleItem: ModuleItemEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(moduleItems: List) + + @Delete + suspend fun delete(moduleItem: ModuleItemEntity) + + @Update + suspend fun update(moduleItem: ModuleItemEntity) + + @Query("SELECT * FROM ModuleItemEntity WHERE moduleId = :moduleId ORDER BY position") + suspend fun findByModuleId(moduleId: Long): List + + @Query("SELECT * FROM ModuleItemEntity WHERE id = :id") + suspend fun findById(id: Long): ModuleItemEntity? + + @Query("SELECT * FROM ModuleItemEntity WHERE type = :type AND contentId = :contentId") + suspend fun findByTypeAndContentId(type: String, contentId: Long): ModuleItemEntity? + + @Query("SELECT * FROM ModuleItemEntity WHERE pageUrl = :pageUrl") + suspend fun findByPageUrl(pageUrl: String): ModuleItemEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleNameDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleNameDao.kt new file mode 100644 index 0000000000..d21ea25f90 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleNameDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ModuleNameEntity + +@Dao +interface ModuleNameDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: ModuleNameEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Delete + suspend fun delete(entity: ModuleNameEntity) + + @Update + suspend fun update(entity: ModuleNameEntity) + + @Query("SELECT * FROM ModuleNameEntity WHERE lockedModuleId = :lockModuleId") + suspend fun findByLockModuleId(lockModuleId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleObjectDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleObjectDao.kt new file mode 100644 index 0000000000..9fe7a49bd7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ModuleObjectDao.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity + +@Dao +interface ModuleObjectDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(moduleObject: ModuleObjectEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(moduleObjects: List) + + @Delete + suspend fun delete(moduleObject: ModuleObjectEntity) + + @Update + suspend fun update(moduleObject: ModuleObjectEntity) + + @Query("SELECT * FROM ModuleObjectEntity WHERE courseId = :courseId ORDER BY position") + suspend fun findByCourseId(courseId: Long): List + + @Query("SELECT * FROM ModuleObjectEntity WHERE id = :id") + suspend fun findById(id: Long): ModuleObjectEntity? + + @Query("DELETE FROM ModuleObjectEntity WHERE courseId = :courseId") + suspend fun deleteAllByCourseId(courseId: Long) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt new file mode 100644 index 0000000000..53ac401873 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PageDao.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.PageEntity + +@Dao +interface PageDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: PageEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Delete + suspend fun delete(entity: PageEntity) + + @Query("DELETE FROM PageEntity WHERE courseId=:id") + suspend fun deleteAllByCourseId(id: Long) + + @Update + suspend fun update(entity: PageEntity) + + @Query("SELECT * FROM PageEntity") + suspend fun findAll(): List + + @Query("SELECT * FROM PageEntity WHERE id=:id") + suspend fun findById(id: Long): PageEntity? + + @Query("SELECT * FROM PageEntity WHERE url=:url") + suspend fun findByUrl(url: String): PageEntity? + + @Query("SELECT * FROM PageEntity WHERE frontPage=1 AND courseId=:courseId") + suspend fun getFrontPage(courseId: Long): PageEntity? + + @Query("SELECT * FROM PageEntity WHERE courseId=:courseId") + suspend fun findByCourseId(courseId: Long): List + + @Query("SELECT * FROM PageEntity WHERE courseId=:courseId AND (url=:pageId OR title=:pageId)") + suspend fun getPageDetails(courseId: Long, pageId: String): PageEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerOverrideDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerOverrideDao.kt new file mode 100644 index 0000000000..165f77f822 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/PlannerOverrideDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.PlannerOverrideEntity + +@Dao +interface PlannerOverrideDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: PlannerOverrideEntity): Long + + @Delete + suspend fun delete(entity: PlannerOverrideEntity) + + @Update + suspend fun update(entity: PlannerOverrideEntity) + + @Query("SELECT * FROM PlannerOverrideEntity WHERE id = :id") + suspend fun findById(id: Long): PlannerOverrideEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/QuizDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/QuizDao.kt new file mode 100644 index 0000000000..ad179ed38f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/QuizDao.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.QuizEntity + +@Dao +interface QuizDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: QuizEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Query("DELETE FROM QuizEntity WHERE courseId = :courseId") + suspend fun deleteAllByCourseId(courseId: Long) + + @Transaction + suspend fun deleteAndInsertAll(entities: List, courseId: Long) { + deleteAllByCourseId(courseId) + insertAll(entities) + } + + @Delete + suspend fun delete(entity: QuizEntity) + + @Update + suspend fun update(entity: QuizEntity) + + @Query("SELECT * FROM QuizEntity WHERE id = :id") + suspend fun findById(id: Long): QuizEntity? + + @Query("SELECT * FROM QuizEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionAssessmentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionAssessmentDao.kt new file mode 100644 index 0000000000..edab1ad04d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionAssessmentDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.RubricCriterionAssessmentEntity + +@Dao +interface RubricCriterionAssessmentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: RubricCriterionAssessmentEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Delete + suspend fun delete(entity: RubricCriterionAssessmentEntity) + + @Update + suspend fun update(entity: RubricCriterionAssessmentEntity) + + @Query("SELECT * FROM RubricCriterionAssessmentEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionDao.kt new file mode 100644 index 0000000000..94cf8fb08b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.RubricCriterionEntity + +@Dao +interface RubricCriterionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: RubricCriterionEntity) + + @Delete + suspend fun delete(entity: RubricCriterionEntity) + + @Update + suspend fun update(entity: RubricCriterionEntity) + + @Query("SELECT * FROM RubricCriterionEntity WHERE id = :id") + suspend fun findById(id: String): RubricCriterionEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionRatingDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionRatingDao.kt new file mode 100644 index 0000000000..833b994fee --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricCriterionRatingDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.RubricCriterionRatingEntity + +@Dao +interface RubricCriterionRatingDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: RubricCriterionRatingEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Delete + suspend fun delete(entity: RubricCriterionRatingEntity) + + @Update + suspend fun update(entity: RubricCriterionRatingEntity) + + @Query("SELECT * FROM RubricCriterionRatingEntity WHERE rubricCriterionId = :rubricCriterionId") + suspend fun findByRubricCriterionId(rubricCriterionId: String): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricSettingsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricSettingsDao.kt new file mode 100644 index 0000000000..977257c171 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/RubricSettingsDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.RubricSettingsEntity + +@Dao +interface RubricSettingsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: RubricSettingsEntity): Long + + @Delete + suspend fun delete(entity: RubricSettingsEntity) + + @Update + suspend fun update(entity: RubricSettingsEntity) + + @Query("SELECT * FROM RubricSettingsEntity WHERE id = :id") + suspend fun findById(id: Long): RubricSettingsEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ScheduleItemAssignmentOverrideDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ScheduleItemAssignmentOverrideDao.kt new file mode 100644 index 0000000000..aad4e166d2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ScheduleItemAssignmentOverrideDao.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ScheduleItemAssignmentOverrideEntity + +@Dao +interface ScheduleItemAssignmentOverrideDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: ScheduleItemAssignmentOverrideEntity) + + @Delete + suspend fun delete(entity: ScheduleItemAssignmentOverrideEntity) + + @Update + suspend fun update(entity: ScheduleItemAssignmentOverrideEntity) + + @Query("SELECT * FROM ScheduleItemAssignmentOverrideEntity WHERE scheduleItemId=:scheduleItemId") + suspend fun findByScheduleItemId(scheduleItemId: String): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ScheduleItemDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ScheduleItemDao.kt new file mode 100644 index 0000000000..206c0412d0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/ScheduleItemDao.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.ScheduleItemEntity + +@Dao +interface ScheduleItemDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: ScheduleItemEntity) + + @Delete + suspend fun delete(entity: ScheduleItemEntity) + + @Query("DELETE FROM ScheduleItemEntity WHERE courseId=:courseId") + suspend fun deleteAllByCourseId(courseId: Long) + + @Update + suspend fun update(entity: ScheduleItemEntity) + + @Query("SELECT * FROM ScheduleItemEntity WHERE id=:id") + suspend fun findById(id: String): ScheduleItemEntity? + + @Query("SELECT * FROM ScheduleItemEntity WHERE contextCode IN (:contextCodes) AND type=:type") + suspend fun findByItemType(contextCodes: List, type: String): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SectionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SectionDao.kt new file mode 100644 index 0000000000..c897deffe0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SectionDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.SectionEntity + +@Dao +interface SectionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: SectionEntity) + + @Upsert(entity = SectionEntity::class) + suspend fun insertOrUpdate(entity: SectionEntity) + + @Delete + suspend fun delete(entity: SectionEntity) + + @Update + suspend fun update(entity: SectionEntity) + + @Query("SELECT * FROM SectionEntity WHERE courseId = :courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/SubmissionCommentDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubmissionCommentDao.kt similarity index 78% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/SubmissionCommentDao.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubmissionCommentDao.kt index 36f42f5a32..788168c5f4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/daos/SubmissionCommentDao.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubmissionCommentDao.kt @@ -1,8 +1,8 @@ -package com.instructure.pandautils.room.common.daos +package com.instructure.pandautils.room.offline.daos import androidx.room.* -import com.instructure.pandautils.room.common.entities.SubmissionCommentEntity -import com.instructure.pandautils.room.common.model.SubmissionCommentWithAttachments +import com.instructure.pandautils.room.offline.entities.SubmissionCommentEntity +import com.instructure.pandautils.room.offline.model.SubmissionCommentWithAttachments @Dao interface SubmissionCommentDao { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubmissionDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubmissionDao.kt new file mode 100644 index 0000000000..820a67b9dd --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SubmissionDao.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.SubmissionEntity + +@Dao +interface SubmissionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: SubmissionEntity): Long + + @Upsert(entity = SubmissionEntity::class) + suspend fun insertOrUpdate(entity: SubmissionEntity) + + @Delete + suspend fun delete(entity: SubmissionEntity) + + @Update + suspend fun update(entity: SubmissionEntity) + + @Query("SELECT * FROM SubmissionEntity WHERE id = :id ORDER BY attempt ASC") + suspend fun findById(id: Long): List + + @Query("SELECT * FROM SubmissionEntity WHERE assignmentId IN (:assignmentIds)") + suspend fun findByAssignmentIds(assignmentIds: List): List + + @Query("SELECT * FROM SubmissionEntity WHERE assignmentId = :assignmentId") + suspend fun findByAssignmentId(assignmentId: Long): SubmissionEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SyncSettingsDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SyncSettingsDao.kt new file mode 100644 index 0000000000..c5bcecf142 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/SyncSettingsDao.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.SyncSettingsEntity + +@Dao +interface SyncSettingsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: SyncSettingsEntity) + + @Update + suspend fun update(entity: SyncSettingsEntity) + + @Query("SELECT * FROM SyncSettingsEntity WHERE id=1") + suspend fun findSyncSettings(): SyncSettingsEntity? + + @Query("SELECT * FROM SyncSettingsEntity WHERE id=1") + fun findSyncSettingsLiveData(): LiveData +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/TabDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/TabDao.kt new file mode 100644 index 0000000000..7561f4bc25 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/TabDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.TabEntity + +@Dao +interface TabDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: TabEntity) + + @Delete + suspend fun delete(entity: TabEntity) + + @Update + suspend fun update(entity: TabEntity) + + @Query("SELECT * FROM TabEntity WHERE courseId=:courseId") + suspend fun findByCourseId(courseId: Long): List +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/TermDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/TermDao.kt new file mode 100644 index 0000000000..6932d2f02d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/TermDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.TermEntity + +@Dao +interface TermDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: TermEntity) + + @Upsert(entity = TermEntity::class) + suspend fun insertOrUpdate(entity: TermEntity) + + @Delete + suspend fun delete(entity: TermEntity) + + @Update + suspend fun update(entity: TermEntity) + + @Query("SELECT * FROM TermEntity WHERE id = :id") + suspend fun findById(id: Long): TermEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/UserCalendarDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/UserCalendarDao.kt new file mode 100644 index 0000000000..9f15ec15c5 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/UserCalendarDao.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.instructure.pandautils.room.offline.entities.UserCalendarEntity + +@Dao +interface UserCalendarDao { + + @Insert + suspend fun insert(entity: UserCalendarEntity) + + @Delete + suspend fun delete(entity: UserCalendarEntity) + + @Update + suspend fun update(entity: UserCalendarEntity) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/UserDao.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/UserDao.kt new file mode 100644 index 0000000000..ef7f8a4a24 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/daos/UserDao.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.daos + +import androidx.room.* +import com.instructure.pandautils.room.offline.entities.UserEntity + +@Dao +interface UserDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: UserEntity) + + @Upsert(entity = UserEntity::class) + suspend fun insertOrUpdate(entity: UserEntity) + + @Delete + suspend fun delete(entity: UserEntity) + + @Update + suspend fun update(entity: UserEntity) + + @Query("SELECT * FROM UserEntity WHERE id = :id") + suspend fun findById(id: Long): UserEntity? +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentDueDateEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentDueDateEntity.kt new file mode 100644 index 0000000000..e9d8c1f434 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentDueDateEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentDueDateEntity( + @PrimaryKey + val assignmentId: Long, + val assignmentOverrideId: Long?, + var dueAt: String?, + val title: String?, + var unlockAt: String?, + var lockAt: String?, + var isBase: Boolean +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt new file mode 100644 index 0000000000..5cb18f0e4a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentEntity.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentGroupEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentGroupId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentEntity( + @PrimaryKey + val id: Long, + val name: String?, + val description: String?, + val submissionTypesRaw: List, + val dueAt: String?, + val pointsPossible: Double, + val courseId: Long, + val isGradeGroupsIndividually: Boolean, + val gradingType: String?, + val needsGradingCount: Long, + val htmlUrl: String?, + val url: String?, + val quizId: Long, + val isUseRubricForGrading: Boolean, + val rubricSettingsId: Long?, + val allowedExtensions: List, + val submissionId: Long?, + val assignmentGroupId: Long, + val position: Int, + val isPeerReviews: Boolean, + // TODO val lockInfo: LockInfo?, + val lockedForUser: Boolean, + val lockAt: String?, + val unlockAt: String?, + val lockExplanation: String?, + val discussionTopicHeaderId: Long?, + val freeFormCriterionComments: Boolean, + val published: Boolean, + val groupCategoryId: Long, + val userSubmitted: Boolean, + val unpublishable: Boolean, + val onlyVisibleToOverrides: Boolean, + val anonymousPeerReviews: Boolean, + val moderatedGrading: Boolean, + val anonymousGrading: Boolean, + val allowedAttempts: Long, + val plannerOverrideId: Long?, + val isStudioEnabled: Boolean, + val inClosedGradingPeriod: Boolean, + val annotatableAttachmentId: Long, + val anonymousSubmissions: Boolean, + val omitFromFinalGrade: Boolean +) { + constructor( + assignment: Assignment, + rubricSettingsId: Long?, + submissionId: Long?, + discussionTopicHeaderId: Long?, + plannerOverrideId: Long? + ) : this( + assignment.id, + assignment.name, + assignment.description, + assignment.submissionTypesRaw, + assignment.dueAt, + assignment.pointsPossible, + assignment.courseId, + assignment.isGradeGroupsIndividually, + assignment.gradingType, + assignment.needsGradingCount, + assignment.htmlUrl, + assignment.url, + assignment.quizId, + assignment.isUseRubricForGrading, + rubricSettingsId, + assignment.allowedExtensions, + submissionId, + assignment.assignmentGroupId, + assignment.position, + assignment.isPeerReviews, + assignment.lockedForUser, + assignment.lockAt, + assignment.unlockAt, + assignment.lockExplanation, + discussionTopicHeaderId, + assignment.freeFormCriterionComments, + assignment.published, + assignment.groupCategoryId, + assignment.userSubmitted, + assignment.unpublishable, + assignment.onlyVisibleToOverrides, + assignment.anonymousPeerReviews, + assignment.moderatedGrading, + assignment.anonymousGrading, + assignment.allowedAttempts, + plannerOverrideId, + assignment.isStudioEnabled, + assignment.inClosedGradingPeriod, + assignment.annotatableAttachmentId, + assignment.anonymousSubmissions, + assignment.omitFromFinalGrade + ) + + fun toApiModel( + rubric: List = emptyList(), + rubricSettings: RubricSettings? = null, + submission: Submission? = null, + lockInfo: LockInfo? = null, + discussionTopicHeader: DiscussionTopicHeader? = null, + scoreStatistics: AssignmentScoreStatistics? = null, + plannerOverride: PlannerOverride? = null + ) = Assignment( + id = id, + name = name, + description = description, + submissionTypesRaw = submissionTypesRaw, + dueAt = dueAt, + pointsPossible = pointsPossible, + courseId = courseId, + isGradeGroupsIndividually = isGradeGroupsIndividually, + gradingType = gradingType, + needsGradingCount = needsGradingCount, + htmlUrl = htmlUrl, + url = url, + quizId = quizId, + rubric = rubric, + isUseRubricForGrading = isUseRubricForGrading, + rubricSettings = rubricSettings, + allowedExtensions = allowedExtensions, + submission = submission, + assignmentGroupId = assignmentGroupId, + position = position, + isPeerReviews = isPeerReviews, + lockInfo = lockInfo, + lockedForUser = lockedForUser, + lockAt = lockAt, + unlockAt = unlockAt, + lockExplanation = lockExplanation, + discussionTopicHeader = discussionTopicHeader, + //TODO + needsGradingCountBySection = emptyList(), + freeFormCriterionComments = freeFormCriterionComments, + published = published, + groupCategoryId = groupCategoryId, + //TODO + allDates = emptyList(), + userSubmitted = userSubmitted, + unpublishable = unpublishable, + //TODO + overrides = null, + onlyVisibleToOverrides = onlyVisibleToOverrides, + anonymousPeerReviews = anonymousPeerReviews, + moderatedGrading = moderatedGrading, + anonymousGrading = anonymousGrading, + scoreStatistics = scoreStatistics, + allowedAttempts = allowedAttempts, + externalToolAttributes = null, + plannerOverride = plannerOverride, + isStudioEnabled = isStudioEnabled, + inClosedGradingPeriod = inClosedGradingPeriod, + annotatableAttachmentId = annotatableAttachmentId, + anonymousSubmissions = anonymousSubmissions, + omitFromFinalGrade = omitFromFinalGrade + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentGroupEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentGroupEntity.kt new file mode 100644 index 0000000000..c7f20a35d4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentGroupEntity.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.GradingRule + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentGroupEntity( + @PrimaryKey + val id: Long, + val name: String?, + val position: Int, + val groupWeight: Double, + val rules: GradingRule?, + val courseId: Long +) { + constructor(assignmentGroup: AssignmentGroup, courseId: Long) : this( + assignmentGroup.id, + assignmentGroup.name, + assignmentGroup.position, + assignmentGroup.groupWeight, + assignmentGroup.rules, + courseId + ) + + fun toApiModel(assignments: List = emptyList()) = AssignmentGroup( + id = id, + name = name, + position = position, + groupWeight = groupWeight, + assignments = assignments, + rules = rules + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentOverrideEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentOverrideEntity.kt new file mode 100644 index 0000000000..a8c7a346fc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentOverrideEntity.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.AssignmentOverride +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentOverrideEntity( + @PrimaryKey + val id: Long, + val assignmentId: Long, + val title: String?, + val dueAt: Date?, + val isAllDay: Boolean, + val allDayDate: String?, + val unlockAt: Date?, + val lockAt: Date?, + val courseSectionId: Long, + val groupId: Long +) { + + constructor(assignmentOverride: AssignmentOverride) : this( + assignmentOverride.id, + assignmentOverride.assignmentId, + assignmentOverride.title, + assignmentOverride.dueAt, + assignmentOverride.isAllDay, + assignmentOverride.allDayDate, + assignmentOverride.unlockAt, + assignmentOverride.lockAt, + assignmentOverride.courseSectionId, + assignmentOverride.groupId + ) + + fun toApiModel() = AssignmentOverride( + id, + assignmentId, + title, + dueAt, + isAllDay, + allDayDate, + unlockAt, + lockAt, + courseSectionId + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentRubricCriterionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentRubricCriterionEntity.kt new file mode 100644 index 0000000000..131599ef05 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentRubricCriterionEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["assignmentId", "rubricId"], + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentRubricCriterionEntity( + val assignmentId: Long, + val rubricId: String +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentScoreStatisticsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentScoreStatisticsEntity.kt new file mode 100644 index 0000000000..5bf43ef029 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentScoreStatisticsEntity.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.AssignmentScoreStatistics + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentScoreStatisticsEntity( + @PrimaryKey + val assignmentId: Long, + val mean: Double, + val min: Double, + val max: Double +) { + constructor(assignmentScoreStatistics: AssignmentScoreStatistics, assignmentId: Long) : this( + assignmentId, + assignmentScoreStatistics.mean, + assignmentScoreStatistics.min, + assignmentScoreStatistics.max + ) + + fun toApiModel() = AssignmentScoreStatistics( + mean = mean, + min = min, + max = max + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentSetEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentSetEntity.kt new file mode 100644 index 0000000000..2af45fc759 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AssignmentSetEntity.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.AssignmentSet +import com.instructure.canvasapi2.models.MasteryPathAssignment + +@Entity( + foreignKeys = [ + ForeignKey( + entity = MasteryPathEntity::class, + parentColumns = ["id"], + childColumns = ["masteryPathId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AssignmentSetEntity( + @PrimaryKey + val id: Long, + val scoringRangeId: Long, + val createdAt: String?, + val updatedAt: String?, + val position: Int, + val masteryPathId: Long +) { + + constructor(assignmentSet: AssignmentSet, masteryPathId: Long) : this( + id = assignmentSet.id, + scoringRangeId = assignmentSet.scoringRangeId, + createdAt = assignmentSet.createdAt, + updatedAt = assignmentSet.updatedAt, + position = assignmentSet.position, + masteryPathId = masteryPathId + ) + + fun toApiModel(assignments: List): AssignmentSet { + return AssignmentSet( + id = id, + scoringRangeId = scoringRangeId, + createdAt = createdAt, + updatedAt = updatedAt, + position = position, + assignments = assignments.toTypedArray() + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AttachmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AttachmentEntity.kt new file mode 100644 index 0000000000..c02008b0e5 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AttachmentEntity.kt @@ -0,0 +1,69 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Attachment +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = SubmissionCommentEntity::class, + parentColumns = ["id"], + childColumns = ["submissionCommentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class AttachmentEntity( + @PrimaryKey val id: Long, + val contentType: String? = null, + val filename: String? = null, + val displayName: String? = null, + val url: String? = null, + val thumbnailUrl: String? = null, + val previewUrl: String? = null, + val createdAt: Date? = null, + val size: Long = 0, + //Used for file upload result + val workerId: String? = null, + //Used for Submission comments + val submissionCommentId: Long? = null, + val submissionId: Long? = null, + val attempt: Long? = null +) { + constructor( + attachment: Attachment, + workerId: String? = null, + submissionCommentId: Long? = null, + submissionId: Long? = null, + attempt: Long? = null + ) : this( + id = attachment.id, + contentType = attachment.contentType, + filename = attachment.filename, + displayName = attachment.displayName, + url = attachment.url, + thumbnailUrl = attachment.thumbnailUrl, + previewUrl = attachment.previewUrl, + createdAt = attachment.createdAt, + size = attachment.size, + workerId = workerId, + submissionCommentId = submissionCommentId, + submissionId = submissionId, + attempt = attempt + ) + + fun toApiModel() = Attachment( + id = id, + contentType = contentType, + filename = filename, + displayName = displayName, + url = url, + thumbnailUrl = thumbnailUrl, + previewUrl = previewUrl, + createdAt = createdAt, + size = size + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AuthorEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AuthorEntity.kt new file mode 100644 index 0000000000..0c4ff2149a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/AuthorEntity.kt @@ -0,0 +1,32 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Author + +@Entity +data class AuthorEntity( + @PrimaryKey val id: Long, + val displayName: String? = null, + val avatarImageUrl: String? = null, + val htmlUrl: String? = null, + val pronouns: String? = null +) { + constructor(author: Author) : this( + author.id, + author.displayName, + author.avatarImageUrl, + author.htmlUrl, + author.pronouns + ) + + fun toApiModel(): Author { + return Author( + id, + displayName, + avatarImageUrl, + htmlUrl, + pronouns + ) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ConferenceEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ConferenceEntity.kt new file mode 100644 index 0000000000..e925c9c24b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ConferenceEntity.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Conference +import com.instructure.canvasapi2.models.ConferenceRecording +import com.instructure.canvasapi2.models.ConferenceUserSettings +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ConferenceEntity( + @PrimaryKey + val id: Long, + val courseId: Long, + val conferenceKey: String?, + val conferenceType: String?, + val description: String?, + val duration: Long, + val endedAt: Date?, + val hasAdvancedSettings: Boolean, + val joinUrl: String?, + val longRunning: Boolean, + val startedAt: Date?, + val title: String?, + val url: String?, + val contextType: String, + val contextId: Long, + val record: Boolean?, + val users: List +) { + constructor(conference: Conference, courseId: Long) : this( + id = conference.id, + courseId = courseId, + conferenceKey = conference.conferenceKey, + conferenceType = conference.conferenceType, + description = conference.description, + duration = conference.duration, + endedAt = conference.endedAt, + hasAdvancedSettings = conference.hasAdvancedSettings, + joinUrl = conference.joinUrl, + longRunning = conference.longRunning, + startedAt = conference.startedAt, + title = conference.title, + url = conference.url, + contextType = conference.contextType, + contextId = conference.contextId, + record = conference.userSettings?.record, + users = conference.users + ) + + fun toApiModel(recordings: List = emptyList()) = Conference( + id = id, + conferenceKey = conferenceKey, + conferenceType = conferenceType, + description = description, + duration = duration, + endedAt = endedAt, + hasAdvancedSettings = hasAdvancedSettings, + joinUrl = joinUrl, + longRunning = longRunning, + startedAt = startedAt, + title = title, + url = url, + recordings = recordings, + contextType = contextType, + contextId = contextId, + userSettings = record?.let { ConferenceUserSettings(it) }, + users = users + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ConferenceRecordingEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ConferenceRecordingEntity.kt new file mode 100644 index 0000000000..1590594f37 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ConferenceRecordingEntity.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.ConferenceRecording + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ConferenceEntity::class, + parentColumns = ["id"], + childColumns = ["conferenceId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ConferenceRecordingEntity( + @PrimaryKey + val recordingId: String, + val conferenceId: Long, + val createdAtMillis: Long, + val durationMinutes: Long, + val playbackUrl: String?, + val title: String +) { + constructor(conferenceRecording: ConferenceRecording, conferenceId: Long) : this( + recordingId = conferenceRecording.recordingId, + conferenceId = conferenceId, + createdAtMillis = conferenceRecording.createdAtMillis, + durationMinutes = conferenceRecording.durationMinutes, + playbackUrl = conferenceRecording.playbackUrl, + title = conferenceRecording.title + ) + + fun toApiModel() = ConferenceRecording( + createdAtMillis = createdAtMillis, + durationMinutes = durationMinutes, + playbackFormats = emptyList(), + playbackUrl = playbackUrl, + recordingId = recordingId, + title = title + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseEntity.kt new file mode 100644 index 0000000000..aa62883570 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseEntity.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = TermEntity::class, + parentColumns = ["id"], + childColumns = ["termId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CourseEntity( + @PrimaryKey + val id: Long, + val name: String, + val originalName: String?, + val courseCode: String?, + val startAt: String?, + val endAt: String?, + var syllabusBody: String?, + val hideFinalGrades: Boolean, + val isPublic: Boolean, + val license: String, + val termId: Long?, + val needsGradingCount: Long, + val isApplyAssignmentGroupWeights: Boolean, + val currentScore: Double?, + val finalScore: Double?, + val currentGrade: String?, + val finalGrade: String?, + val isFavorite: Boolean, + val accessRestrictedByDate: Boolean, + val imageUrl: String?, + val bannerImageUrl: String?, + val isWeightedGradingPeriods: Boolean, + val hasGradingPeriods: Boolean, + val homePage: String?, + val restrictEnrollmentsToCourseDate: Boolean, + val workflowState: String?, + val homeroomCourse: Boolean, + val courseColor: String?, + val gradingScheme: List? +) { + constructor(course: Course) : this( + course.id, + course.name, + course.originalName, + course.courseCode, + course.startAt, + course.endAt, + course.syllabusBody, + course.hideFinalGrades, + course.isPublic, + course.license?.name ?: Course.License.PRIVATE_COPYRIGHTED.name, + course.term?.id, + course.needsGradingCount, + course.isApplyAssignmentGroupWeights, + course.currentScore, + course.finalScore, + course.currentGrade, + course.finalGrade, + course.isFavorite, + course.accessRestrictedByDate, + course.imageUrl, + course.bannerImageUrl, + course.isWeightedGradingPeriods, + course.hasGradingPeriods, + course.homePage?.name, + course.restrictEnrollmentsToCourseDate, + course.workflowState?.name, + course.homeroomCourse, + course.courseColor, + course.gradingScheme + ) + + fun toApiModel( + term: Term? = null, + enrollments: MutableList? = null, + sections: List
= emptyList(), + gradingPeriods: List? = null, + tabs: List? = null, + settings: CourseSettings? = null + ): Course { + return Course( + id = id, + name = name, + originalName = originalName, + courseCode = courseCode, + startAt = startAt, + endAt = endAt, + syllabusBody = syllabusBody, + hideFinalGrades = hideFinalGrades, + isPublic = isPublic, + license = Course.License.valueOf(license), + term = term, + enrollments = enrollments, + needsGradingCount = needsGradingCount, + isApplyAssignmentGroupWeights = isApplyAssignmentGroupWeights, + currentScore = currentScore, + finalScore = finalScore, + currentGrade = currentGrade, + finalGrade = finalGrade, + isFavorite = isFavorite, + accessRestrictedByDate = accessRestrictedByDate, + imageUrl = imageUrl, + bannerImageUrl = bannerImageUrl, + isWeightedGradingPeriods = isWeightedGradingPeriods, + hasGradingPeriods = hasGradingPeriods, + sections = sections, + homePage = homePage?.let { Course.HomePage.valueOf(homePage) }, + restrictEnrollmentsToCourseDate = restrictEnrollmentsToCourseDate, + workflowState = workflowState?.let { Course.WorkflowState.valueOf(it) }, + homeroomCourse = homeroomCourse, + courseColor = courseColor, + gradingPeriods = gradingPeriods, + tabs = tabs, + settings = settings, + gradingSchemeRaw = gradingScheme?.map { listOf(it.name, it.value) } + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseFeaturesEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseFeaturesEntity.kt new file mode 100644 index 0000000000..a5d879141c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseFeaturesEntity.kt @@ -0,0 +1,21 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CourseFeaturesEntity( + @PrimaryKey + val id: Long, + val features: List +) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseFilesEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseFilesEntity.kt new file mode 100644 index 0000000000..b673f3b927 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseFilesEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["courseId", "url"], + foreignKeys = [ + ForeignKey( + entity = CourseSyncSettingsEntity::class, + parentColumns = ["courseId"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CourseFilesEntity( + val courseId: Long, + val url: String +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseGradingPeriodEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseGradingPeriodEntity.kt new file mode 100644 index 0000000000..c117331a8c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseGradingPeriodEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import com.instructure.pandautils.room.offline.entities.CourseEntity +import com.instructure.pandautils.room.offline.entities.GradingPeriodEntity + +@Entity( + primaryKeys = ["courseId", "gradingPeriodId"], + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = GradingPeriodEntity::class, + parentColumns = ["id"], + childColumns = ["gradingPeriodId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CourseGradingPeriodEntity( + val courseId: Long, + val gradingPeriodId: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSettingsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSettingsEntity.kt new file mode 100644 index 0000000000..d60dd827d0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSettingsEntity.kt @@ -0,0 +1,33 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.CourseSettings + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class CourseSettingsEntity( + @PrimaryKey + val courseId: Long, + val courseSummary: Boolean?, + val restrictQuantitativeData: Boolean, +) { + constructor(courseSettings: CourseSettings, courseId: Long) : this( + courseId, + courseSettings.courseSummary, + courseSettings.restrictQuantitativeData + ) + + fun toApiModel(): CourseSettings { + return CourseSettings(courseSummary, restrictQuantitativeData) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt new file mode 100644 index 0000000000..c7e4872a84 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncProgressEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.pandautils.features.offline.sync.ProgressState +import com.instructure.pandautils.features.offline.sync.TabSyncData + +const val TAB_PROGRESS_SIZE = 100 * 1000 + +@Entity +data class CourseSyncProgressEntity( + @PrimaryKey + val courseId: Long, + val workerId: String, + val courseName: String, + val tabs: Map = emptyMap(), + val additionalFilesStarted: Boolean = false, + val progressState: ProgressState = ProgressState.STARTING, +) { + fun totalSize() = tabs.size * TAB_PROGRESS_SIZE + + fun downloadedSize() = tabs.count { it.value.state == ProgressState.COMPLETED } * TAB_PROGRESS_SIZE +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncSettingsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncSettingsEntity.kt new file mode 100644 index 0000000000..4da00bcbf8 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/CourseSyncSettingsEntity.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Tab + +@Entity +data class CourseSyncSettingsEntity( + @PrimaryKey + val courseId: Long, + val courseName: String, + val fullContentSync: Boolean, + val tabs: Map = TABS.associateWith { false }, + val fullFileSync: Boolean = false +) { + + fun isTabSelected(tabId: String): Boolean { + val isSelected = if (tabId == Tab.FILES_ID) { + fullFileSync + } else { + tabs[tabId] ?: false + } + return fullContentSync || isSelected + } + + fun areAnyTabsSelected(tabIds: Set): Boolean { + return tabIds.any { isTabSelected(it) } + } + + val allTabsEnabled: Boolean + get() = tabs.values.all { it } && fullFileSync + + val anySyncEnabled: Boolean + get() = fullContentSync || fullFileSync || tabs.values.any { it } + + companion object { + val TABS = setOf( + Tab.ASSIGNMENTS_ID, + Tab.PAGES_ID, + Tab.GRADES_ID, + Tab.SYLLABUS_ID, + Tab.ANNOUNCEMENTS_ID, + Tab.DISCUSSIONS_ID, + Tab.CONFERENCES_ID, + Tab.PEOPLE_ID, + Tab.MODULES_ID, + Tab.QUIZZES_ID + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DashboardCardEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DashboardCardEntity.kt new file mode 100644 index 0000000000..bfc3c599b0 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DashboardCardEntity.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.DashboardCard + +@Entity +data class DashboardCardEntity( + @PrimaryKey + val id: Long, + val isK5Subject: Boolean, + val shortName: String?, + val originalName: String?, + val courseCode: String?, + val position: Int +) { + constructor(dashboardCard: DashboardCard) : this( + dashboardCard.id, + dashboardCard.isK5Subject, + dashboardCard.shortName, + dashboardCard.originalName, + dashboardCard.courseCode, + dashboardCard.position + ) + + fun toApiModel(): DashboardCard { + return DashboardCard( + id, + isK5Subject, + shortName, + originalName, + courseCode, + position + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionEntryAttachmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionEntryAttachmentEntity.kt new file mode 100644 index 0000000000..a142ddebce --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionEntryAttachmentEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["discussionEntryId", "remoteFileId"], + foreignKeys = [ + ForeignKey( + entity = DiscussionEntryEntity::class, + parentColumns = ["id"], + childColumns = ["discussionEntryId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = RemoteFileEntity::class, + parentColumns = ["id"], + childColumns = ["remoteFileId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class DiscussionEntryAttachmentEntity( + val discussionEntryId: Long, + val remoteFileId: Long +) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionEntryEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionEntryEntity.kt new file mode 100644 index 0000000000..b69a149985 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionEntryEntity.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionParticipant + +@Entity +data class DiscussionEntryEntity( + @PrimaryKey + val id: Long, + var unread: Boolean, + var updatedAt: String?, + val createdAt: String?, + var authorId: Long?, + var description: String?, + val userId: Long, + var parentId: Long, + var message: String?, + var deleted: Boolean, + var totalChildren: Int, + var unreadChildren: Int, + val ratingCount: Int, + var ratingSum: Int, + val editorId: Long, + var _hasRated: Boolean, + var replyIds: List, +) { + constructor(discussionEntry: DiscussionEntry, replyIds: List = emptyList()): this( + discussionEntry.id, + discussionEntry.unread, + discussionEntry.updatedAt, + discussionEntry.createdAt, + discussionEntry.author?.id, + discussionEntry.description, + discussionEntry.userId, + discussionEntry.parentId, + discussionEntry.message, + discussionEntry.deleted, + discussionEntry.totalChildren, + discussionEntry.unreadChildren, + discussionEntry.ratingCount, + discussionEntry.ratingSum, + discussionEntry.editorId, + discussionEntry._hasRated, + replyIds, + ) + + fun toApiModel(author: DiscussionParticipant? = null, replyDiscussionEntries: MutableList = mutableListOf()): DiscussionEntry { + return DiscussionEntry( + id = id, + unread = unread, + updatedAt = updatedAt, + createdAt = createdAt, + author = author, + description = description, + userId = userId, + parentId = parentId, + message = message, + deleted = deleted, + totalChildren = totalChildren, + unreadChildren = unreadChildren, + ratingCount = ratingCount, + ratingSum = ratingSum, + editorId = editorId, + _hasRated = _hasRated, + replies = replyDiscussionEntries, + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionParticipantEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionParticipantEntity.kt new file mode 100644 index 0000000000..4cf447b331 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionParticipantEntity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.DiscussionParticipant + +@Entity +data class DiscussionParticipantEntity( + @PrimaryKey + var id: Long, + var displayName: String?, + val pronouns: String?, + var avatarImageUrl: String?, + var htmlUrl: String? +) { + constructor(discussionParticipant: DiscussionParticipant) : this( + discussionParticipant.id, + discussionParticipant.displayName, + discussionParticipant.pronouns, + discussionParticipant.avatarImageUrl, + discussionParticipant.htmlUrl + ) + + fun toApiModel() = DiscussionParticipant( + id = id, + displayName = displayName, + pronouns = pronouns, + avatarImageUrl = avatarImageUrl, + htmlUrl = htmlUrl + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt new file mode 100644 index 0000000000..dd4d4b3e65 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicEntity.kt @@ -0,0 +1,33 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopic + +@Entity +data class DiscussionTopicEntity( + @PrimaryKey + val id: Long, + val unreadEntries: MutableList, + val participantIds: List, + val viewIds: List, +) { + constructor(discussionTopic: DiscussionTopic, participantIds: List, viewIds: List, topicId: Long): this( + id = topicId, + unreadEntries = discussionTopic.unreadEntries, + participantIds = participantIds, + viewIds = viewIds + ) + + fun toApiModel(participants: List, views: List): DiscussionTopic { + return DiscussionTopic( + unreadEntries = unreadEntries, + participants = participants, + unreadEntriesMap = hashMapOf(), + entryRatings = hashMapOf(), + views = views?.toMutableList().orEmpty().toMutableList() + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt new file mode 100644 index 0000000000..5eea233e1d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicHeaderEntity.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.DiscussionTopicPermission +import com.instructure.pandautils.utils.orDefault +import java.util.Date + +@Entity( + foreignKeys = [ + ForeignKey( + entity = DiscussionParticipantEntity::class, + parentColumns = ["id"], + childColumns = ["authorId"], + onDelete = ForeignKey.SET_NULL + ), + ForeignKey( + entity = DiscussionTopicPermissionEntity::class, + parentColumns = ["id"], + childColumns = ["permissionId"], + onDelete = ForeignKey.SET_NULL + ), + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ), + ] +) +data class DiscussionTopicHeaderEntity( + @PrimaryKey + val id: Long, + val courseId: Long, + var discussionType: String?, + var title: String?, + var message: String?, + var htmlUrl: String?, + var postedDate: Date?, + var delayedPostDate: Date?, + var lastReplyDate: Date?, + var requireInitialPost: Boolean, + var discussionSubentryCount: Int, + var readState: String?, + var unreadCount: Int, + var position: Int, + var assignmentId: Long?, + var locked: Boolean, + var lockedForUser: Boolean, + var lockExplanation: String?, + var pinned: Boolean, + var authorId: Long?, + var podcastUrl: String?, + var groupCategoryId: String?, + var announcement: Boolean, + var permissionId: Long?, + //TODO var groupTopicChildren: List, + // TODO var lockInfo: LockInfo?, + var published: Boolean, + var allowRating: Boolean, + var onlyGradersCanRate: Boolean, + var sortByRating: Boolean, + var subscribed: Boolean, + var lockAt: Date?, + var userCanSeePosts: Boolean, + var specificSections: String?, + var anonymousState: String? +) { + constructor(discussionTopicHeader: DiscussionTopicHeader, courseId: Long, permissionId: Long? = null) : this( + discussionTopicHeader.id, + courseId, + discussionTopicHeader.discussionType, + discussionTopicHeader.title, + discussionTopicHeader.message, + discussionTopicHeader.htmlUrl, + discussionTopicHeader.postedDate, + discussionTopicHeader.delayedPostDate, + discussionTopicHeader.lastReplyDate, + discussionTopicHeader.requireInitialPost, + discussionTopicHeader.discussionSubentryCount, + discussionTopicHeader.readState, + discussionTopicHeader.unreadCount, + discussionTopicHeader.position, + discussionTopicHeader.assignmentId, + discussionTopicHeader.locked, + discussionTopicHeader.lockedForUser, + discussionTopicHeader.lockExplanation, + discussionTopicHeader.pinned, + discussionTopicHeader.author?.id, + discussionTopicHeader.podcastUrl, + discussionTopicHeader.groupCategoryId, + discussionTopicHeader.announcement, + permissionId, + discussionTopicHeader.published, + discussionTopicHeader.allowRating, + discussionTopicHeader.onlyGradersCanRate, + discussionTopicHeader.sortByRating, + discussionTopicHeader.subscribed, + discussionTopicHeader.lockAt, + discussionTopicHeader.userCanSeePosts, + discussionTopicHeader.specificSections, + discussionTopicHeader.anonymousState + ) + + fun toApiModel( + author: DiscussionParticipant? = null, + assignment: Assignment? = null, + permissions: DiscussionTopicPermission? = null + ) = DiscussionTopicHeader( + id = id, + discussionType = discussionType, + title = title, + message = message, + htmlUrl = htmlUrl, + postedDate = postedDate, + delayedPostDate = delayedPostDate, + lastReplyDate = lastReplyDate, + requireInitialPost = requireInitialPost, + discussionSubentryCount = discussionSubentryCount, + readState = readState, + unreadCount = unreadCount, + position = position, + assignmentId = assignmentId.orDefault(), + locked = locked, + lockedForUser = lockedForUser, + lockExplanation = lockExplanation, + pinned = pinned, + author = author, + podcastUrl = podcastUrl, + groupCategoryId = groupCategoryId, + announcement = announcement, + groupTopicChildren = emptyList(), + //TODO + attachments = mutableListOf(), + //TODO + permissions = permissions, + assignment = assignment, + //TODO + lockInfo = null, + published = published, + allowRating = allowRating, + onlyGradersCanRate = onlyGradersCanRate, + sortByRating = sortByRating, + subscribed = subscribed, + lockAt = lockAt, + userCanSeePosts = userCanSeePosts, + specificSections = specificSections, + //TODO + sections = null, + anonymousState = anonymousState, + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicPermissionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicPermissionEntity.kt new file mode 100644 index 0000000000..2f50306f91 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicPermissionEntity.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.DiscussionTopicPermission + +@Entity( + foreignKeys = [ + ForeignKey( + entity = DiscussionTopicHeaderEntity::class, + parentColumns = ["id"], + childColumns = ["discussionTopicHeaderId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class DiscussionTopicPermissionEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val discussionTopicHeaderId: Long, + val attach: Boolean, + val update: Boolean, + val delete: Boolean, + val reply: Boolean +) { + constructor(permission: DiscussionTopicPermission, discussionTopicHeaderId: Long) : this( + 0, + discussionTopicHeaderId, + permission.attach, + permission.update, + permission.delete, + permission.reply + ) + + fun toApiModel(): DiscussionTopicPermission { + return DiscussionTopicPermission( + attach, + update, + delete, + reply + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicRemoteFileEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicRemoteFileEntity.kt new file mode 100644 index 0000000000..96c6236b4e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicRemoteFileEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["discussionId", "remoteFileId"], + foreignKeys = [ + ForeignKey( + entity = DiscussionTopicHeaderEntity::class, + parentColumns = ["id"], + childColumns = ["discussionId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = RemoteFileEntity::class, + parentColumns = ["id"], + childColumns = ["remoteFileId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class DiscussionTopicRemoteFileEntity( + val discussionId: Long, + val remoteFileId: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicSectionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicSectionEntity.kt new file mode 100644 index 0000000000..6e2bc8b1d6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/DiscussionTopicSectionEntity.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["discussionTopicId", "sectionId"], + foreignKeys = [ + ForeignKey( + entity = DiscussionTopicHeaderEntity::class, + parentColumns = ["id"], + childColumns = ["discussionTopicId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = SectionEntity::class, + parentColumns = ["id"], + childColumns = ["sectionId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class DiscussionTopicSectionEntity( + val discussionTopicId: Long, + val sectionId: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/EditDashboardItemEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/EditDashboardItemEntity.kt new file mode 100644 index 0000000000..a70162d2f2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/EditDashboardItemEntity.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Course + +@Entity +data class EditDashboardItemEntity( + @PrimaryKey + val courseId: Long, + val name: String, + val isFavorite: Boolean, + val enrollmentState: EnrollmentState, + val position: Int +) { + constructor(course: Course, enrollmentState: EnrollmentState, position: Int) : this( + courseId = course.id, + name = course.name, + isFavorite = course.isFavorite, + enrollmentState = enrollmentState, + position = position + ) + + fun toCourse(): Course { + return Course( + id = courseId, + name = name, + isFavorite = isFavorite + ) + } +} + +enum class EnrollmentState { + CURRENT, FUTURE, PAST +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/EnrollmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/EnrollmentEntity.kt new file mode 100644 index 0000000000..011b484b1b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/EnrollmentEntity.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.Grades +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.orDefault +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = UserEntity::class, + parentColumns = ["id"], + childColumns = ["userId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = UserEntity::class, + parentColumns = ["id"], + childColumns = ["observedUserId"], + onDelete = ForeignKey.SET_NULL + ), + ForeignKey( + entity = SectionEntity::class, + parentColumns = ["id"], + childColumns = ["courseSectionId"], + onDelete = ForeignKey.SET_NULL + ), + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class EnrollmentEntity( + @PrimaryKey + val id: Long, + val role: String, + val type: String, + val courseId: Long?, + val courseSectionId: Long?, + val enrollmentState: String?, + val userId: Long, + val computedCurrentScore: Double?, + val computedFinalScore: Double?, + val computedCurrentGrade: String?, + val computedFinalGrade: String?, + val multipleGradingPeriodsEnabled: Boolean, + val totalsForAllGradingPeriodsOption: Boolean, + val currentPeriodComputedCurrentScore: Double?, + val currentPeriodComputedFinalScore: Double?, + val currentPeriodComputedCurrentGrade: String?, + val currentPeriodComputedFinalGrade: String?, + val currentGradingPeriodId: Long, + val currentGradingPeriodTitle: String?, + val associatedUserId: Long, + val lastActivityAt: Date?, + val limitPrivilegesToCourseSection: Boolean, + val observedUserId: Long? +) { + constructor(enrollment: Enrollment, courseId: Long? = null, courseSectionId: Long? = null, observedUserId: Long?) : this( + enrollment.id, + enrollment.role?.name ?: Enrollment.EnrollmentType.NoEnrollment.name, + enrollment.type?.name ?: Enrollment.EnrollmentType.NoEnrollment.name, + if (enrollment.courseId != 0L) enrollment.courseId else courseId, + if (enrollment.courseSectionId != 0L) enrollment.courseSectionId else courseSectionId, + enrollment.enrollmentState, + enrollment.userId, + enrollment.computedCurrentScore, + enrollment.computedFinalScore, + enrollment.computedCurrentGrade, + enrollment.computedFinalGrade, + enrollment.multipleGradingPeriodsEnabled, + enrollment.totalsForAllGradingPeriodsOption, + enrollment.currentPeriodComputedCurrentScore, + enrollment.currentPeriodComputedFinalScore, + enrollment.currentPeriodComputedCurrentGrade, + enrollment.currentPeriodComputedFinalGrade, + enrollment.currentGradingPeriodId, + enrollment.currentGradingPeriodTitle, + enrollment.associatedUserId, + enrollment.lastActivityAt, + enrollment.limitPrivilegesToCourseSection, + observedUserId + ) + + fun toApiModel( + grades: Grades? = null, + observedUser: User? = null, + user: User? = null + ) = Enrollment( + id = id, + role = Enrollment.EnrollmentType.valueOf(role), + type = Enrollment.EnrollmentType.valueOf(type), + courseId = courseId.orDefault(), + courseSectionId = courseSectionId.orDefault(), + enrollmentState = enrollmentState, + userId = userId, + grades = grades, + computedCurrentScore = computedCurrentScore, + computedFinalScore = computedFinalScore, + computedCurrentGrade = computedCurrentGrade, + computedFinalGrade = computedFinalGrade, + multipleGradingPeriodsEnabled = multipleGradingPeriodsEnabled, + totalsForAllGradingPeriodsOption = totalsForAllGradingPeriodsOption, + currentPeriodComputedCurrentScore = currentPeriodComputedCurrentScore, + currentPeriodComputedFinalScore = currentPeriodComputedFinalScore, + currentPeriodComputedCurrentGrade = currentPeriodComputedCurrentGrade, + currentPeriodComputedFinalGrade = currentPeriodComputedFinalGrade, + currentGradingPeriodId = currentGradingPeriodId, + currentGradingPeriodTitle = currentGradingPeriodTitle, + associatedUserId = associatedUserId, + lastActivityAt = lastActivityAt, + limitPrivilegesToCourseSection = limitPrivilegesToCourseSection, + observedUser = observedUser, + user = user + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ExternalToolAttributesEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ExternalToolAttributesEntity.kt new file mode 100644 index 0000000000..e456ced035 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ExternalToolAttributesEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ExternalToolAttributesEntity( + @PrimaryKey + val assignmentId: Long, + val url: String?, + val newTab: Boolean, + val resourceLinkid: String?, + val contentId: Long? +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileFolderEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileFolderEntity.kt new file mode 100644 index 0000000000..d699cebab1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileFolderEntity.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.FileFolder +import com.instructure.canvasapi2.models.LockInfo +import java.util.Date + +@Entity +data class FileFolderEntity( + @PrimaryKey + val id: Long, + val createdDate: Date?, + val updatedDate: Date?, + var unlockDate: Date?, + var lockDate: Date?, + var isLocked: Boolean, + var isHidden: Boolean, + val isLockedForUser: Boolean, + val isHiddenForUser: Boolean, + + // File Attributes + val folderId: Long, + val size: Long, + val contentType: String?, + val url: String?, + val displayName: String?, + val thumbnailUrl: String?, + // Folder Attributes + val parentFolderId: Long, + val contextId: Long, + val filesCount: Int, + val position: Int, + val foldersCount: Int, + val contextType: String?, + val name: String?, + val foldersUrl: String?, + val filesUrl: String?, + val fullName: String?, + val forSubmissions: Boolean, + val canUpload: Boolean +) { + + constructor(fileFolder: FileFolder) : this( + fileFolder.id, + fileFolder.createdDate, + fileFolder.updatedDate, + fileFolder.unlockDate, + fileFolder.lockDate, + fileFolder.isLocked, + fileFolder.isHidden, + fileFolder.isLockedForUser, + fileFolder.isHiddenForUser, + fileFolder.folderId, + fileFolder.size, + fileFolder.contentType, + fileFolder.url, + fileFolder.displayName, + fileFolder.thumbnailUrl, + fileFolder.parentFolderId, + fileFolder.contextId, + fileFolder.filesCount, + fileFolder.position, + fileFolder.foldersCount, + fileFolder.contextType, + fileFolder.name, + fileFolder.foldersUrl, + fileFolder.filesUrl, + fileFolder.fullName, + fileFolder.forSubmissions, + fileFolder.canUpload + ) + + fun toApiModel(): FileFolder { + return FileFolder( + id, + createdDate, + updatedDate, + unlockDate, + lockDate, + isLocked, + isHidden, + isLockedForUser, + isHiddenForUser, + folderId, + size, + contentType, + url, + displayName, + thumbnailUrl, + null, + parentFolderId, + contextId, + filesCount, + position, + foldersCount, + contextType, + name, + foldersUrl, + filesUrl, + fullName, + null, + forSubmissions, + canUpload + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt new file mode 100644 index 0000000000..212095f5e3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncProgressEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.pandautils.features.offline.sync.ProgressState + +@Entity( + foreignKeys = [ + androidx.room.ForeignKey( + entity = CourseSyncProgressEntity::class, + parentColumns = ["courseId"], + childColumns = ["courseId"], + onDelete = androidx.room.ForeignKey.CASCADE + ) + ] +) +data class FileSyncProgressEntity( + @PrimaryKey + val workerId: String, + val courseId: Long, + val fileName: String, + val progress: Int, + val fileSize: Long, + val additionalFile: Boolean = false, + val progressState: ProgressState, + val fileId: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncSettingsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncSettingsEntity.kt new file mode 100644 index 0000000000..c4e6eb358b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/FileSyncSettingsEntity.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseSyncSettingsEntity::class, + parentColumns = ["courseId"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class FileSyncSettingsEntity( + @PrimaryKey + val id: Long, + val fileName: String?, + val courseId: Long, + val url: String? +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GradesEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GradesEntity.kt new file mode 100644 index 0000000000..74de20a052 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GradesEntity.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Grades + +@Entity( + foreignKeys = [ + ForeignKey( + entity = EnrollmentEntity::class, + parentColumns = ["id"], + childColumns = ["enrollmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +class GradesEntity( + @PrimaryKey + val enrollmentId: Long, + val htmlUrl: String, + val currentScore: Double?, + val finalScore: Double?, + val currentGrade: String?, + val finalGrade: String?, +) { + constructor(grades: Grades, enrollmentId: Long) : this( + enrollmentId, + grades.htmlUrl.orEmpty(), + grades.currentScore, + grades.finalScore, + grades.currentGrade, + grades.finalGrade, + ) + + fun toApiModel() = Grades( + htmlUrl = htmlUrl, + currentScore = currentScore, + finalScore = finalScore, + currentGrade = currentGrade, + finalGrade = finalGrade + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GradingPeriodEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GradingPeriodEntity.kt new file mode 100644 index 0000000000..4fe19ea659 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GradingPeriodEntity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.GradingPeriod + +@Entity +data class GradingPeriodEntity( + @PrimaryKey + val id: Long, + val title: String?, + val startDate: String?, + val endDate: String?, + val weight: Double, +) { + constructor(gradingPeriod: GradingPeriod) : this( + gradingPeriod.id, + gradingPeriod.title, + gradingPeriod.startDate, + gradingPeriod.endDate, + gradingPeriod.weight + ) + + fun toApiModel() = GradingPeriod( + id = id, + title = title, + startDate = startDate, + endDate = endDate, + weight = weight + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GroupEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GroupEntity.kt new file mode 100644 index 0000000000..47e966809d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GroupEntity.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Group + +@Entity +data class GroupEntity( + @PrimaryKey + val id: Long, + val name: String?, + val description: String?, + val avatarUrl: String?, + val isPublic: Boolean, + val membersCount: Int, + val joinLevel: String?, + val courseId: Long, + val accountId: Long, + val role: String?, + val groupCategoryId: Long, + val storageQuotaMb: Long, + val isFavorite: Boolean, + val concluded: Boolean, + val canAccess: Boolean? +) { + constructor(group: Group) : this( + group.id, + group.name, + group.description, + group.avatarUrl, + group.isPublic, + group.membersCount, + group.joinLevel?.name, + group.courseId, + group.accountId, + group.role?.name, + group.groupCategoryId, + group.storageQuotaMb, + group.isFavorite, + group.concluded, + group.canAccess + ) + + fun toApiModel() = Group( + id = id, + name = name, + description = description, + avatarUrl = avatarUrl, + isPublic = isPublic, + membersCount = membersCount, + joinLevel = joinLevel?.let { Group.JoinLevel.valueOf(it) }, + courseId = courseId, + accountId = accountId, + role = role?.let { Group.GroupRole.valueOf(it) }, + groupCategoryId = groupCategoryId, + storageQuotaMb = storageQuotaMb, + isFavorite = isFavorite, + concluded = concluded, + canAccess = canAccess + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GroupUserEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GroupUserEntity.kt new file mode 100644 index 0000000000..ecf49b4ec6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/GroupUserEntity.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = GroupEntity::class, + parentColumns = ["id"], + childColumns = ["groupId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = UserEntity::class, + parentColumns = ["id"], + childColumns = ["userId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class GroupUserEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val groupId: Long, + val userId: Long +) { + constructor(groupId: Long, userId: Long) : this(0, groupId, userId) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LocalFileEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LocalFileEntity.kt new file mode 100644 index 0000000000..399e741fa1 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LocalFileEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import java.util.Date +@Entity +data class LocalFileEntity( + @PrimaryKey + val id: Long, + val courseId: Long, + val createdDate: Date, + val path: String +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt new file mode 100644 index 0000000000..b6a6ffc1ea --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockInfoEntity.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.LockInfo +import com.instructure.canvasapi2.models.LockedModule + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ModuleContentDetailsEntity::class, + parentColumns = ["id"], + childColumns = ["moduleId"], + onDelete = ForeignKey.CASCADE, + deferred = true + ), + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE, + deferred = true + ), + ForeignKey( + entity = PageEntity::class, + parentColumns = ["id"], + childColumns = ["pageId"], + onDelete = ForeignKey.CASCADE, + deferred = true + ) + ], + indices = [Index("lockedModuleId", unique = true)] +) +data class LockInfoEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val modulePrerequisiteNames: List?, + val unlockAt: String?, + val lockedModuleId: Long?, + val assignmentId: Long?, + val moduleId: Long?, + val pageId: Long? +) { + constructor(lockInfo: LockInfo, assignmentId: Long? = null, moduleId: Long? = null, pageId: Long? = null) : this( + modulePrerequisiteNames = lockInfo.modulePrerequisiteNames, + unlockAt = lockInfo.unlockAt, + lockedModuleId = lockInfo.contextModule?.id, + assignmentId = assignmentId, + moduleId = moduleId, + pageId = pageId + ) + + fun toApiModel( + lockedModule: LockedModule? = null + ) = LockInfo( + modulePrerequisiteNames = ArrayList(modulePrerequisiteNames.orEmpty()), + contextModule = lockedModule, + unlockAt = unlockAt + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt new file mode 100644 index 0000000000..4cb4ac8d0a --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/LockedModuleEntity.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.LockedModule +import com.instructure.canvasapi2.models.ModuleCompletionRequirement +import com.instructure.canvasapi2.models.ModuleName + +@Entity( + foreignKeys = [ + ForeignKey( + entity = LockInfoEntity::class, + parentColumns = ["lockedModuleId"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class LockedModuleEntity( + @PrimaryKey + val id: Long, + val contextId: Long, + val contextType: String?, + val name: String?, + val unlockAt: String?, + val isRequireSequentialProgress: Boolean +) { + constructor(lockedModule: LockedModule) : this( + id = lockedModule.id, + contextId = lockedModule.contextId, + contextType = lockedModule.contextType, + name = lockedModule.name, + unlockAt = lockedModule.unlockAt, + isRequireSequentialProgress = lockedModule.isRequireSequentialProgress + ) + + fun toApiModel( + prerequisites: List? = null, + completionRequirements: List = emptyList() + ) = LockedModule( + id = id, + contextId = contextId, + contextType = contextType, + name = name, + unlockAt = unlockAt, + isRequireSequentialProgress = isRequireSequentialProgress, + prerequisites = prerequisites, + completionRequirements = completionRequirements + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MasteryPathAssignmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MasteryPathAssignmentEntity.kt new file mode 100644 index 0000000000..77a21c2552 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MasteryPathAssignmentEntity.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.MasteryPathAssignment + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentSetEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentSetId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class MasteryPathAssignmentEntity( + @PrimaryKey + val id: Long, + val assignmentId: Long, + val createdAt: String?, + val updatedAt: String?, + val overrideId: Long, + val assignmentSetId: Long, + val position: Int +) { + + constructor(masteryPathAssignment: MasteryPathAssignment) : this( + id = masteryPathAssignment.id, + assignmentId = masteryPathAssignment.assignmentId, + createdAt = masteryPathAssignment.createdAt, + updatedAt = masteryPathAssignment.updatedAt, + overrideId = masteryPathAssignment.overrideId, + assignmentSetId = masteryPathAssignment.assignmentSetId, + position = masteryPathAssignment.position + ) + + fun toApiModel(assignment: Assignment?): MasteryPathAssignment { + return MasteryPathAssignment( + id = id, + assignmentId = assignmentId, + createdAt = createdAt, + updatedAt = updatedAt, + overrideId = overrideId, + assignmentSetId = assignmentSetId, + position = position, + model = assignment + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MasteryPathEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MasteryPathEntity.kt new file mode 100644 index 0000000000..4bfe7ddfdc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MasteryPathEntity.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.AssignmentSet +import com.instructure.canvasapi2.models.MasteryPath + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ModuleItemEntity::class, + parentColumns = ["id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class MasteryPathEntity( + @PrimaryKey + val id: Long, + val isLocked: Boolean, + val selectedSetId: Long +) { + + constructor(masteryPath: MasteryPath, moduleItemId: Long) : this( + id = moduleItemId, // This will always be a 1on1 relationship with the moduleItem so we use the same id + isLocked = masteryPath.isLocked, + selectedSetId = masteryPath.selectedSetId + ) + + fun toApiModel(assignmentSets: List) = MasteryPath( + isLocked = isLocked, + assignmentSets = assignmentSets.toTypedArray(), + selectedSetId = selectedSetId + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MediaCommentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MediaCommentEntity.kt new file mode 100644 index 0000000000..2cd57e2993 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/MediaCommentEntity.kt @@ -0,0 +1,46 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.MediaComment +import com.instructure.pandautils.room.offline.entities.SubmissionEntity + +@Entity( + foreignKeys = [ + ForeignKey( + entity = SubmissionEntity::class, + parentColumns = ["id", "attempt"], + childColumns = ["submissionId", "attemptId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class MediaCommentEntity( + @PrimaryKey + val mediaId: String, + val submissionId: Long, + val attemptId: Long, + val displayName: String? = null, + val url: String? = null, + val mediaType: String? = null, + val contentType: String? = null +) { + constructor(mediaComment: MediaComment, submissionId: Long, attemptId: Long) : this( + mediaComment.mediaId!!, + submissionId, + attemptId, + mediaComment.displayName, + mediaComment.url, + mediaComment.mediaType?.name, + mediaComment.contentType + ) + + fun toApiModel() = MediaComment( + mediaId, + displayName, + url, + mediaType?.let { MediaComment.MediaType.valueOf(it) }, + contentType + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleCompletionRequirementEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleCompletionRequirementEntity.kt new file mode 100644 index 0000000000..62f66a761e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleCompletionRequirementEntity.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.ModuleCompletionRequirement + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ModuleObjectEntity::class, + parentColumns = ["id"], + childColumns = ["moduleId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ModuleCompletionRequirementEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val type: String?, + val minScore: Double, + val maxScore: Double, + val completed: Boolean?, + val moduleId: Long +) { + constructor(moduleCompletionRequirement: ModuleCompletionRequirement, moduleId: Long, moduleItemId: Long? = null) : this( + // In some api calls we don't receive the id of this entity, but it corresponds to the id of the module item so in that case we can use that + id = if (moduleCompletionRequirement.id == 0L) moduleItemId ?: 0 else moduleCompletionRequirement.id, + type = moduleCompletionRequirement.type, + minScore = moduleCompletionRequirement.minScore, + maxScore = moduleCompletionRequirement.maxScore, + completed = moduleCompletionRequirement.completed, + moduleId = moduleId + ) + + fun toApiModel() = ModuleCompletionRequirement( + id = id, + type = type, + minScore = minScore, + maxScore = maxScore, + completed = completed ?: false + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleContentDetailsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleContentDetailsEntity.kt new file mode 100644 index 0000000000..ccf828fed7 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleContentDetailsEntity.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.LockInfo +import com.instructure.canvasapi2.models.ModuleContentDetails + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ModuleItemEntity::class, + parentColumns = ["id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ModuleContentDetailsEntity( + @PrimaryKey + val id: Long, + val pointsPossible: String?, + val dueAt: String?, + val unlockAt: String?, + val lockAt: String?, + val lockedForUser: Boolean, + val lockExplanation: String? +) { + constructor(moduleContentDetails: ModuleContentDetails, moduleItemId: Long) : this( + id = moduleItemId, // This will always be a 1on1 relationship with the moduleItem so we use the same id + pointsPossible = moduleContentDetails.pointsPossible, + dueAt = moduleContentDetails.dueAt, + unlockAt = moduleContentDetails.unlockAt, + lockAt = moduleContentDetails.lockAt, + lockedForUser = moduleContentDetails.lockedForUser, + lockExplanation = moduleContentDetails.lockExplanation, + ) + + fun toApiModel(lockInfo: LockInfo?) = ModuleContentDetails( + pointsPossible = pointsPossible, + dueAt = dueAt, + unlockAt = unlockAt, + lockAt = lockAt, + lockedForUser = lockedForUser, + lockExplanation = lockExplanation, + lockInfo = lockInfo + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleItemEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleItemEntity.kt new file mode 100644 index 0000000000..c9e1cf5bef --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleItemEntity.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.MasteryPath +import com.instructure.canvasapi2.models.ModuleCompletionRequirement +import com.instructure.canvasapi2.models.ModuleContentDetails +import com.instructure.canvasapi2.models.ModuleItem + +@Entity( + foreignKeys = [ + ForeignKey( + entity = ModuleObjectEntity::class, + parentColumns = ["id"], + childColumns = ["moduleId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ModuleItemEntity( + @PrimaryKey + val id: Long, + val moduleId: Long, + var position: Int, + val title: String?, + val indent: Int, + val type: String?, + val htmlUrl: String?, + val url: String?, + val published: Boolean?, + val contentId: Long, + val externalUrl: String?, + val pageUrl: String? +) { + constructor(moduleItem: ModuleItem, moduleId: Long) : this( + id = moduleItem.id, + moduleId = moduleId, + position = moduleItem.position, + title = moduleItem.title, + indent = moduleItem.indent, + type = moduleItem.type, + htmlUrl = moduleItem.htmlUrl, + url = moduleItem.url, + published = moduleItem.published, + contentId = moduleItem.contentId, + externalUrl = moduleItem.externalUrl, + pageUrl = moduleItem.pageUrl + ) + + fun toApiModel( + completionRequirement: ModuleCompletionRequirement?, + moduleContentDetails: ModuleContentDetails?, + masteryPath: MasteryPath? + ) = ModuleItem( + id = id, + moduleId = moduleId, + position = position, + title = title, + indent = indent, + type = type, + htmlUrl = htmlUrl, + url = url, + completionRequirement = completionRequirement, + moduleDetails = moduleContentDetails, + published = published, + contentId = contentId, + externalUrl = externalUrl, + pageUrl = pageUrl, + masteryPaths = masteryPath + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleNameEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleNameEntity.kt new file mode 100644 index 0000000000..17f090ccac --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleNameEntity.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.ModuleName + +@Entity( + foreignKeys = [ + ForeignKey( + entity = LockedModuleEntity::class, + parentColumns = ["id"], + childColumns = ["lockedModuleId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ModuleNameEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val name: String?, + val lockedModuleId: Long +) { + constructor(moduleName: ModuleName, lockedModuleId: Long) : this( + name = moduleName.name, + lockedModuleId = lockedModuleId + ) + + fun toApiModel() = ModuleName( + name = name + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleObjectEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleObjectEntity.kt new file mode 100644 index 0000000000..6188d99898 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ModuleObjectEntity.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ModuleObjectEntity( + @PrimaryKey + val id: Long, + val position: Int, + val name: String?, + val unlockAt: String? = null, + val sequentialProgress: Boolean = false, + val prerequisiteIds: LongArray? = null, + val state: String? = null, + val completedAt: String? = null, + val published: Boolean? = null, + val itemCount: Int = 0, + val itemsUrl: String = "", + val courseId: Long +) { + constructor(module: ModuleObject, courseId: Long) : this( + id = module.id, + position = module.position, + name = module.name, + unlockAt = module.unlockAt, + sequentialProgress = module.sequentialProgress, + prerequisiteIds = module.prerequisiteIds, + state = module.state, + completedAt = module.completedAt, + published = module.published, + itemCount = module.itemCount, + itemsUrl = module.itemsUrl, + courseId = courseId + ) + + fun toApiModel(items: List) = ModuleObject( + id = id, + position = position, + name = name, + unlockAt = unlockAt, + sequentialProgress = sequentialProgress, + prerequisiteIds = prerequisiteIds, + state = state, + completedAt = completedAt, + published = published, + itemCount = itemCount, + itemsUrl = itemsUrl, + items = items + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/NeedsGradingCountEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/NeedsGradingCountEntity.kt new file mode 100644 index 0000000000..e4a8f1366e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/NeedsGradingCountEntity.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = SectionEntity::class, + parentColumns = ["id"], + childColumns = ["sectionId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class NeedsGradingCountEntity( + @PrimaryKey + val sectionId: Long, + var needsGradingCount: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PageEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PageEntity.kt new file mode 100644 index 0000000000..9422339308 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PageEntity.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.LockInfo +import com.instructure.canvasapi2.models.Page +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class PageEntity( + @PrimaryKey + val id: Long, + val url: String?, + val title: String?, + val createdAt: Date?, + val updatedAt: Date?, + val hideFromStudents: Boolean, + val status: String?, + val body: String?, + val frontPage: Boolean, + val published: Boolean, + val editingRoles: String?, + val htmlUrl: String?, + val courseId: Long +) { + + constructor(page: Page, courseId: Long) : this( + page.id, + page.url, + page.title, + page.createdAt, + page.updatedAt, + page.hideFromStudents, + page.status, + page.body, + page.frontPage, + page.published, + page.editingRoles, + page.htmlUrl, + courseId + ) + + fun toApiModel(lockInfo: LockInfo? = null): Page { + return Page( + id = id, + url = url, + title = title, + createdAt = createdAt, + updatedAt = updatedAt, + hideFromStudents = hideFromStudents, + status = status, + body = body, + frontPage = frontPage, + lockInfo = lockInfo, + published = published, + editingRoles = editingRoles, + htmlUrl = htmlUrl + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerOverrideEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerOverrideEntity.kt new file mode 100644 index 0000000000..a924841125 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/PlannerOverrideEntity.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.PlannableType +import com.instructure.canvasapi2.models.PlannerOverride + +@Entity +data class PlannerOverrideEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val plannableType: String, + val plannableId: Long, + val dismissed: Boolean, + val markedComplete: Boolean +) { + constructor(plannerOverride: PlannerOverride) : this( + plannerOverride.id ?: 0L, + plannerOverride.plannableType.name, + plannerOverride.plannableId, + plannerOverride.dismissed, + plannerOverride.markedComplete + ) + + fun toApiModel() = PlannerOverride( + id = id, + plannableType = PlannableType.valueOf(plannableType), + plannableId = plannableId, + dismissed = dismissed, + markedComplete = markedComplete + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/QuizEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/QuizEntity.kt new file mode 100644 index 0000000000..6c8c16da96 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/QuizEntity.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Quiz + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class QuizEntity( + @PrimaryKey + val id: Long, + val title: String?, + val mobileUrl: String?, + val htmlUrl: String?, + val description: String?, + val quizType: String?, + val assignmentGroupId: Long, + // TODO val lockInfo: LockInfo?, + // TODO val permissions: QuizPermission?, + val allowedAttempts: Int, + val questionCount: Int, + val pointsPossible: String?, + val isLockQuestionsAfterAnswering: Boolean, + val dueAt: String?, + val timeLimit: Int, + val shuffleAnswers: Boolean, + val showCorrectAnswers: Boolean, + val scoringPolicy: String?, + val accessCode: String?, + val ipFilter: String?, + val lockedForUser: Boolean, + val lockExplanation: String?, + val hideResults: String?, + val showCorrectAnswersAt: String?, + val hideCorrectAnswersAt: String?, + val unlockAt: String?, + val oneTimeResults: Boolean, + val lockAt: String?, + val questionTypes: List, + val hasAccessCode: Boolean, + val oneQuestionAtATime: Boolean, + val requireLockdownBrowser: Boolean, + val requireLockdownBrowserForResults: Boolean, + val allowAnonymousSubmissions: Boolean, + val published: Boolean, + val assignmentId: Long, + val isOnlyVisibleToOverrides: Boolean, + val unpublishable: Boolean, + val courseId: Long +) { + constructor(quiz: Quiz, courseId: Long) : this( + id = quiz.id, + title = quiz.title, + mobileUrl = quiz.mobileUrl, + htmlUrl = quiz.htmlUrl, + description = quiz.description, + quizType = quiz.quizType, + assignmentGroupId = quiz.assignmentGroupId, + allowedAttempts = quiz.allowedAttempts, + questionCount = quiz.questionCount, + pointsPossible = quiz.pointsPossible, + isLockQuestionsAfterAnswering = quiz.isLockQuestionsAfterAnswering, + dueAt = quiz.dueAt, + timeLimit = quiz.timeLimit, + shuffleAnswers = quiz.shuffleAnswers, + showCorrectAnswers = quiz.showCorrectAnswers, + scoringPolicy = quiz.scoringPolicy, + accessCode = quiz.accessCode, + ipFilter = quiz.ipFilter, + lockedForUser = quiz.lockedForUser, + lockExplanation = quiz.lockExplanation, + hideResults = quiz.hideResults, + showCorrectAnswersAt = quiz.showCorrectAnswersAt, + hideCorrectAnswersAt = quiz.hideCorrectAnswersAt, + unlockAt = quiz.unlockAt, + oneTimeResults = quiz.oneTimeResults, + lockAt = quiz.lockAt, + questionTypes = quiz.questionTypes, + hasAccessCode = quiz.hasAccessCode, + oneQuestionAtATime = quiz.oneQuestionAtATime, + requireLockdownBrowser = quiz.requireLockdownBrowser, + requireLockdownBrowserForResults = quiz.requireLockdownBrowserForResults, + allowAnonymousSubmissions = quiz.allowAnonymousSubmissions, + published = quiz.published, + assignmentId = quiz.assignmentId, + isOnlyVisibleToOverrides = quiz.isOnlyVisibleToOverrides, + unpublishable = quiz.unpublishable, + courseId = courseId, + ) + + fun toApiModel() = Quiz( + id = id, + title = title, + mobileUrl = mobileUrl, + htmlUrl = htmlUrl, + description = description, + quizType = quizType, + assignmentGroupId = assignmentGroupId, + //TODO + lockInfo = null, + //TODO + permissions = null, + allowedAttempts = allowedAttempts, + questionCount = questionCount, + pointsPossible = pointsPossible, + isLockQuestionsAfterAnswering = isLockQuestionsAfterAnswering, + dueAt = dueAt, + timeLimit = timeLimit, + shuffleAnswers = shuffleAnswers, + showCorrectAnswers = showCorrectAnswers, + scoringPolicy = scoringPolicy, + accessCode = accessCode, + ipFilter = ipFilter, + lockedForUser = lockedForUser, + lockExplanation = lockExplanation, + hideResults = hideResults, + showCorrectAnswersAt = showCorrectAnswersAt, + hideCorrectAnswersAt = hideCorrectAnswersAt, + unlockAt = unlockAt, + oneTimeResults = oneTimeResults, + lockAt = lockAt, + questionTypes = questionTypes, + hasAccessCode = hasAccessCode, + oneQuestionAtATime = oneQuestionAtATime, + requireLockdownBrowser = requireLockdownBrowser, + requireLockdownBrowserForResults = requireLockdownBrowserForResults, + allowAnonymousSubmissions = allowAnonymousSubmissions, + published = published, + assignmentId = assignmentId, + isOnlyVisibleToOverrides = isOnlyVisibleToOverrides, + unpublishable = unpublishable + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RemoteFileEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RemoteFileEntity.kt new file mode 100644 index 0000000000..87d0980a4e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RemoteFileEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class RemoteFileEntity( + @PrimaryKey + val id: Long, + val folderId: Long, + val displayName: String?, + val fileName: String?, + val contentType: String?, + val url: String?, + val size: Long, + val createdAt: String?, + val updatedAt: String?, + val unlockAt: String?, + val locked: Boolean, + val hidden: Boolean, + val lockAt: String?, + val hiddenForUser: Boolean, + val thumbnailUrl: String?, + val modifiedAt: String?, + val lockedForUser: Boolean, + val previewUrl: String?, + val lockExplanation: String? +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionAssessmentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionAssessmentEntity.kt new file mode 100644 index 0000000000..3545ed79d9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionAssessmentEntity.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import com.instructure.canvasapi2.models.RubricCriterionAssessment + +@Entity( + primaryKeys = ["id", "assignmentId"], + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class RubricCriterionAssessmentEntity( + val id: String, + val assignmentId: Long, + val ratingId: String?, + val points: Double?, + val comments: String? +) { + constructor(rubricCriterionAssessment: RubricCriterionAssessment, id: String, assignmentId: Long) : this( + id = id, + assignmentId = assignmentId, + ratingId = rubricCriterionAssessment.ratingId, + points = rubricCriterionAssessment.points, + comments = rubricCriterionAssessment.comments + ) + + fun toApiModel() = RubricCriterionAssessment( + ratingId = ratingId, + points = points, + comments = comments + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionEntity.kt new file mode 100644 index 0000000000..d5013c0bdf --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionEntity.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.RubricCriterion +import com.instructure.canvasapi2.models.RubricCriterionRating + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class RubricCriterionEntity( + @PrimaryKey + val id: String, + val description: String?, + val longDescription: String?, + val points: Double, + val criterionUseRange: Boolean, + val ignoreForScoring: Boolean, + val assignmentId: Long +) { + constructor(rubricCriterion: RubricCriterion, assignmentId: Long) : this( + rubricCriterion.id.orEmpty(), + rubricCriterion.description, + rubricCriterion.longDescription, + rubricCriterion.points, + rubricCriterion.criterionUseRange, + rubricCriterion.ignoreForScoring, + assignmentId + ) + + fun toApiModel(ratings: List = listOf()) = RubricCriterion( + id = id, + description = description, + longDescription = longDescription, + points = points, + ratings = ratings.toMutableList(), + criterionUseRange = criterionUseRange, + ignoreForScoring = ignoreForScoring + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionRatingEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionRatingEntity.kt new file mode 100644 index 0000000000..b1ed2dfcb9 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricCriterionRatingEntity.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import com.instructure.canvasapi2.models.RubricCriterionRating + +@Entity( + primaryKeys = ["id", "rubricCriterionId"], + foreignKeys = [ + ForeignKey( + entity = RubricCriterionEntity::class, + parentColumns = ["id"], + childColumns = ["rubricCriterionId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class RubricCriterionRatingEntity( + val id: String, + val description: String?, + val longDescription: String?, + val points: Double, + val rubricCriterionId: String +) { + constructor(rubricCriterionRating: RubricCriterionRating, rubricCriterionId: String) : this( + rubricCriterionRating.id.orEmpty(), + rubricCriterionRating.description, + rubricCriterionRating.longDescription, + rubricCriterionRating.points, + rubricCriterionId + ) + + fun toApiModel() = RubricCriterionRating( + id = id, + description = description, + longDescription = longDescription, + points = points + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricSettingsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricSettingsEntity.kt new file mode 100644 index 0000000000..8d0cd3c355 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/RubricSettingsEntity.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.RubricSettings + +@Entity( + foreignKeys = [ + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class RubricSettingsEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val contextId: Long, + val contextType: String?, + val pointsPossible: Double, + val title: String, + val isReusable: Boolean, + val isPublic: Boolean, + val isReadOnly: Boolean, + val freeFormCriterionComments: Boolean, + val hideScoreTotal: Boolean, + val hidePoints: Boolean, + val assignmentId: Long +) { + constructor(rubricSettings: RubricSettings, assignmentId: Long) : this( + rubricSettings.id ?: 0L, + rubricSettings.contextId, + rubricSettings.contextType, + rubricSettings.pointsPossible, + rubricSettings.title, + rubricSettings.isReusable, + rubricSettings.isPublic, + rubricSettings.isReadOnly, + rubricSettings.freeFormCriterionComments, + rubricSettings.hideScoreTotal, + rubricSettings.hidePoints, + assignmentId + ) + + fun toApiModel() = RubricSettings( + id = id, + contextId = contextId, + contextType = contextType, + pointsPossible = pointsPossible, + title = title, + isReusable = isReusable, + isPublic = isPublic, + isReadOnly = isReadOnly, + freeFormCriterionComments = freeFormCriterionComments, + hideScoreTotal = hideScoreTotal, + hidePoints = hidePoints + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ScheduleItemAssignmentOverrideEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ScheduleItemAssignmentOverrideEntity.kt new file mode 100644 index 0000000000..89336d5f3b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ScheduleItemAssignmentOverrideEntity.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["assignmentOverrideId", "scheduleItemId"], + foreignKeys = [ + ForeignKey( + entity = AssignmentOverrideEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentOverrideId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = ScheduleItemEntity::class, + parentColumns = ["id"], + childColumns = ["scheduleItemId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ScheduleItemAssignmentOverrideEntity( + val assignmentOverrideId: Long, + val scheduleItemId: String +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ScheduleItemEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ScheduleItemEntity.kt new file mode 100644 index 0000000000..30bbc81f2c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/ScheduleItemEntity.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentOverride +import com.instructure.canvasapi2.models.ScheduleItem + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ScheduleItemEntity( + @PrimaryKey + val id: String, + val title: String?, + val description: String?, + val startAt: String?, + val endAt: String?, + val isAllDay: Boolean, + val allDayAt: String?, + val locationAddress: String?, + val locationName: String?, + val htmlUrl: String?, + val contextCode: String?, + val effectiveContextCode: String?, + val isHidden: Boolean, + val importantDates: Boolean, + val assignmentId: Long?, + val type: String, + val itemType: String?, + val courseId: Long +) { + + constructor(scheduleItem: ScheduleItem, courseId: Long) : this( + scheduleItem.itemId, + scheduleItem.title, + scheduleItem.description, + scheduleItem.startAt, + scheduleItem.endAt, + scheduleItem.isAllDay, + scheduleItem.allDayAt, + scheduleItem.locationAddress, + scheduleItem.locationName, + scheduleItem.htmlUrl, + scheduleItem.contextCode, + scheduleItem.effectiveContextCode, + scheduleItem.isHidden, + scheduleItem.importantDates, + scheduleItem.assignment?.id, + scheduleItem.type, + scheduleItem.itemType?.name, + courseId + ) + + fun toApiModel(assignmentOverrides: List?, assignment: Assignment?): ScheduleItem { + return ScheduleItem( + itemId = id, + title = title, + description = description, + startAt = startAt, + endAt = endAt, + isAllDay = isAllDay, + allDayAt = allDayAt, + locationAddress = locationAddress, + locationName = locationName, + htmlUrl = htmlUrl, + contextCode = contextCode, + effectiveContextCode = effectiveContextCode, + isHidden = isHidden, + assignmentOverrides = assignmentOverrides, + importantDates = importantDates, + itemType = itemType?.let { ScheduleItem.Type.valueOf(it) } ?: ScheduleItem.Type.TYPE_CALENDAR, + assignment = assignment + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SectionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SectionEntity.kt new file mode 100644 index 0000000000..85f719b8ce --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SectionEntity.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Section +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.orDefault + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SectionEntity( + @PrimaryKey + val id: Long, + var name: String, + val courseId: Long?, + val startAt: String?, + val endAt: String?, + val totalStudents: Int, + val restrictEnrollmentsToSectionDates: Boolean +) { + constructor(section: Section, courseId: Long? = null) : this( + section.id, + section.name, + if (section.courseId != 0L) section.courseId else courseId, + section.startAt, + section.endAt, + section.totalStudents, + section.restrictEnrollmentsToSectionDates + ) + + fun toApiModel( + students: List? = null + ) = Section( + id = id, + name = name, + courseId = courseId.orDefault(), + startAt = startAt, + endAt = endAt, + students = students, + totalStudents = totalStudents, + restrictEnrollmentsToSectionDates = restrictEnrollmentsToSectionDates + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionCommentEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionCommentEntity.kt new file mode 100644 index 0000000000..8daf88c4a2 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionCommentEntity.kt @@ -0,0 +1,42 @@ +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.SubmissionComment +import com.instructure.pandautils.room.offline.entities.SubmissionEntity +import java.util.* + +@Entity( + foreignKeys = [ + ForeignKey( + entity = SubmissionEntity::class, + parentColumns = ["id", "attempt"], + childColumns = ["submissionId", "attemptId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SubmissionCommentEntity( + @PrimaryKey val id: Long = 0, + val authorId: Long = 0, + val authorName: String? = null, + val authorPronouns: String? = null, + val comment: String? = null, + val createdAt: Date? = null, + val mediaCommentId: String? = null, + val attemptId: Long? = null, + val submissionId: Long? = null +) { + constructor(submissionComment: SubmissionComment, submissionId: Long, attemptId: Long) : this( + id = submissionComment.id, + authorId = submissionComment.authorId, + authorName = submissionComment.authorName, + authorPronouns = submissionComment.authorPronouns, + comment = submissionComment.comment, + createdAt = submissionComment.createdAt, + mediaCommentId = submissionComment.mediaComment?.mediaId, + attemptId = attemptId, + submissionId = submissionId + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionDiscussionEntryEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionDiscussionEntryEntity.kt new file mode 100644 index 0000000000..8bb209c4c6 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionDiscussionEntryEntity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["submissionId", "discussionEntryId"], + foreignKeys = [ + ForeignKey( + entity = DiscussionEntryEntity::class, + parentColumns = ["id"], + childColumns = ["discussionEntryId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SubmissionDiscussionEntryEntity( + val submissionId: Long, + val discussionEntryId: Long +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt new file mode 100644 index 0000000000..133b820365 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SubmissionEntity.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import com.instructure.canvasapi2.models.* +import com.instructure.pandautils.utils.orDefault +import java.util.* + +@Entity( + primaryKeys = ["id", "attempt"], + foreignKeys = [ + ForeignKey( + entity = GroupEntity::class, + parentColumns = ["id"], + childColumns = ["groupId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = AssignmentEntity::class, + parentColumns = ["id"], + childColumns = ["assignmentId"], + onDelete = ForeignKey.CASCADE, + deferred = true + ), + ForeignKey( + entity = UserEntity::class, + parentColumns = ["id"], + childColumns = ["userId"], + onDelete = ForeignKey.SET_NULL + ) + ] +) +data class SubmissionEntity( + val id: Long, + val grade: String?, + val score: Double, + val attempt: Long, + val submittedAt: Date?, + val commentCreated: Date?, + val mediaContentType: String?, + val mediaCommentUrl: String?, + val mediaCommentDisplay: String?, + val body: String?, + val isGradeMatchesCurrentSubmission: Boolean, + val workflowState: String?, + val submissionType: String?, + val previewUrl: String?, + val url: String?, + val late: Boolean, + val excused: Boolean, + val missing: Boolean, + val mediaCommentId: String?, + val assignmentId: Long, + val userId: Long?, + val graderId: Long?, + val groupId: Long?, + val pointsDeducted: Double?, + val enteredScore: Double, + val enteredGrade: String?, + val postedAt: Date?, + val gradingPeriodId: Long? +) { + constructor(submission: Submission, groupId: Long?, mediaCommentId: String?) : this( + id = submission.id, + grade = submission.grade, + score = submission.score, + attempt = submission.attempt, + submittedAt = submission.submittedAt, + commentCreated = submission.commentCreated, + mediaContentType = submission.mediaContentType, + mediaCommentUrl = submission.mediaCommentUrl, + mediaCommentDisplay = submission.mediaCommentDisplay, + body = submission.body, + isGradeMatchesCurrentSubmission = submission.isGradeMatchesCurrentSubmission, + workflowState = submission.workflowState, + submissionType = submission.submissionType, + previewUrl = submission.previewUrl, + url = submission.url, + late = submission.late, + excused = submission.excused, + missing = submission.missing, + mediaCommentId = mediaCommentId, + assignmentId = submission.assignmentId, + userId = if (submission.userId == 0L) null else submission.userId, + graderId = if (submission.graderId == 0L) null else submission.graderId, + groupId = groupId, + pointsDeducted = submission.pointsDeducted, + enteredScore = submission.enteredScore, + enteredGrade = submission.enteredGrade, + postedAt = submission.postedAt, + gradingPeriodId = submission.gradingPeriodId + ) + + fun toApiModel( + submissionHistory: List = emptyList(), + submissionComments: List = emptyList(), + attachments: List = emptyList(), + rubricAssessment: HashMap = hashMapOf(), + mediaComment: MediaComment? = null, + assignment: Assignment? = null, + user: User? = null, + group: Group? = null + ) = Submission( + id = id, + grade = grade, + score = score, + attempt = attempt, + submittedAt = submittedAt, + submissionComments = submissionComments, + commentCreated = commentCreated, + mediaContentType = mediaContentType, + mediaCommentUrl = mediaCommentUrl, + mediaCommentDisplay = mediaCommentDisplay, + submissionHistory = submissionHistory, + attachments = ArrayList(attachments), + body = body, + rubricAssessment = rubricAssessment, + isGradeMatchesCurrentSubmission = isGradeMatchesCurrentSubmission, + workflowState = workflowState, + submissionType = submissionType, + previewUrl = previewUrl, + url = url, + late = late, + excused = excused, + missing = missing, + mediaComment = mediaComment, + assignmentId = assignmentId, + assignment = assignment, + userId = userId.orDefault(), + graderId = graderId.orDefault(), + user = user, + //TODO + discussionEntries = arrayListOf(), + group = group, + pointsDeducted = pointsDeducted, + enteredScore = enteredScore, + enteredGrade = enteredGrade, + postedAt = postedAt, + gradingPeriodId = gradingPeriodId + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SyncSettingsEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SyncSettingsEntity.kt new file mode 100644 index 0000000000..01ee9d6cda --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/SyncSettingsEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency + +@Entity +data class SyncSettingsEntity( + @PrimaryKey + val id: Long = 1, + val autoSyncEnabled: Boolean, + val syncFrequency: SyncFrequency, + val wifiOnly: Boolean +) \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/TabEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/TabEntity.kt new file mode 100644 index 0000000000..b7263b4edc --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/TabEntity.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.ForeignKey +import com.instructure.canvasapi2.models.Tab + +@Entity( + primaryKeys = ["id", "courseId"], + foreignKeys = [ForeignKey( + entity = CourseEntity::class, + parentColumns = ["id"], + childColumns = ["courseId"], + onDelete = ForeignKey.CASCADE + )] +) +data class TabEntity( + val id: String, + val label: String?, + val type: String, + val htmlUrl: String?, + val externalUrl: String?, + val visibility: String, + val isHidden: Boolean, + val position: Int, + val ltiUrl: String, + val courseId: Long +) { + constructor(tab: Tab, courseId: Long) : this( + tab.tabId, + tab.label, + tab.type, + tab.htmlUrl, + tab.externalUrl, + tab.visibility, + tab.isHidden, + tab.position, + tab.ltiUrl, + courseId + ) + + fun toApiModel() = Tab( + tabId = id, + label = label, + type = type, + htmlUrl = htmlUrl, + externalUrl = externalUrl, + visibility = visibility, + isHidden = isHidden, + position = position, + ltiUrl = ltiUrl + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/TermEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/TermEntity.kt new file mode 100644 index 0000000000..094d76fd6d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/TermEntity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Term + +@Entity +data class TermEntity( + @PrimaryKey + val id: Long, + val name: String?, + val startAt: String?, + val endAt: String?, + val isGroupTerm: Boolean +) { + constructor(term: Term) : this( + term.id, + term.name, + term.startAt, + term.endAt, + term.isGroupTerm + ) + + fun toApiModel() = Term( + id = id, + name = name, + startAt = startAt, + endAt = endAt, + isGroupTerm = isGroupTerm + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/UserCalendarEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/UserCalendarEntity.kt new file mode 100644 index 0000000000..2421e3c45c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/UserCalendarEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.UserCalendar + +@Entity +data class UserCalendarEntity( + @PrimaryKey(autoGenerate = true) + val id: Long, + val ics: String, +) { + constructor(userCalendar: UserCalendar): this( + 0, + userCalendar.ics + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/UserEntity.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/UserEntity.kt new file mode 100644 index 0000000000..68e61b873b --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/entities/UserEntity.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User + +@Entity +data class UserEntity( + @PrimaryKey + val id: Long, + val name: String, + val shortName: String?, + val loginId: String?, + val avatarUrl: String?, + val primaryEmail: String?, + val email: String?, + val sortableName: String?, + val bio: String?, + val enrollmentIndex: Int, + val lastLogin: String?, + val locale: String?, + val effective_locale: String?, + val pronouns: String?, + val k5User: Boolean, + val rootAccount: String?, + val isFakeStudent: Boolean +) { + constructor(user: User) : this( + user.id, + user.name, + user.shortName, + user.loginId, + user.avatarUrl, + user.primaryEmail, + user.email, + user.sortableName, + user.bio, + user.enrollmentIndex, + user.lastLogin, + user.locale, + user.effective_locale, + user.pronouns, + user.k5User, + user.rootAccount, + user.isFakeStudent + ) + + fun toApiModel(enrollments: List = emptyList()) = User( + id = id, + name = name, + shortName = shortName, + loginId = loginId, + avatarUrl = avatarUrl, + primaryEmail = primaryEmail, + email = email, + sortableName = sortableName, + bio = bio, + enrollments = enrollments, + enrollmentIndex = enrollmentIndex, + lastLogin = lastLogin, + locale = locale, + effective_locale = effective_locale, + pronouns = pronouns, + k5User = k5User, + rootAccount = rootAccount, + isFakeStudent = isFakeStudent, + calendar = null + ) +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt new file mode 100644 index 0000000000..888ac63d26 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/AssignmentFacade.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.PlannerOverride +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.* +import com.instructure.pandautils.room.offline.entities.* + +class AssignmentFacade( + private val assignmentGroupDao: AssignmentGroupDao, + private val assignmentDao: AssignmentDao, + private val plannerOverrideDao: PlannerOverrideDao, + private val rubricSettingsDao: RubricSettingsDao, + private val submissionFacade: SubmissionFacade, + private val discussionTopicHeaderFacade: DiscussionTopicHeaderFacade, + private val assignmentScoreStatisticsDao: AssignmentScoreStatisticsDao, + private val rubricCriterionDao: RubricCriterionDao, + private val lockInfoFacade: LockInfoFacade, + private val rubricCriterionRatingDao: RubricCriterionRatingDao, + private val assignmentRubricCriterionDao: AssignmentRubricCriterionDao, + private val offlineDatabase: OfflineDatabase +) { + + suspend fun insertAssignmentGroups(assignmentGroups: List, courseId: Long) { + offlineDatabase.withTransaction { + deleteAllByCourseId(courseId) + assignmentGroups.forEach { assignmentGroup -> + assignmentGroupDao.insert(AssignmentGroupEntity(assignmentGroup, courseId)) + assignmentGroup.assignments.forEach { assignment -> + insertAssignment(assignment) + } + } + } + } + + suspend fun insertAssignment(assignment: Assignment) { + val plannerOverrideId = insertPlannerOverride(assignment.plannerOverride) + + val discussionTopicHeaderId = assignment.discussionTopicHeader?.let { + discussionTopicHeaderFacade.insertDiscussion(it, assignment.courseId) + } + + val assignmentEntity = AssignmentEntity( + assignment = assignment, + rubricSettingsId = assignment.rubricSettings?.id, + submissionId = assignment.submission?.id, + discussionTopicHeaderId = discussionTopicHeaderId, + plannerOverrideId = plannerOverrideId, + ) + + assignmentDao.insertOrUpdate(assignmentEntity) + + assignment.rubricSettings?.let { + rubricSettingsDao.insert(RubricSettingsEntity(it, assignment.id)) + } + + assignment.submission?.let { + submissionFacade.insertSubmission(it) + } + + assignment.scoreStatistics?.let { + assignmentScoreStatisticsDao.insert(AssignmentScoreStatisticsEntity(it, assignment.id)) + } + + assignment.rubric?.forEach { rubricCriterion -> + rubricCriterionDao.insert(RubricCriterionEntity(rubricCriterion, assignment.id)) + rubricCriterionRatingDao.insertAll(rubricCriterion.ratings.map { + RubricCriterionRatingEntity(it, rubricCriterion.id.orEmpty()) + }) + assignmentRubricCriterionDao.insert( + AssignmentRubricCriterionEntity(assignment.id, rubricCriterion.id.orEmpty()) + ) + } + + assignment.lockInfo?.let { + lockInfoFacade.insertLockInfoForAssignment(it, assignment.id) + } + } + + private suspend fun insertPlannerOverride(plannerOverride: PlannerOverride?): Long? { + return plannerOverride?.let { + plannerOverrideDao.insert(PlannerOverrideEntity(it)) + } + } + + suspend fun getAssignmentById(id: Long): Assignment? { + return assignmentDao.findById(id)?.let { createFullApiModelFromEntity(it) } + } + + suspend fun getAssignmentGroupsWithAssignments( + courseId: Long + ): List { + val assignments = assignmentDao.findByCourseId(courseId).map { createFullApiModelFromEntity(it) } + return assignments.groupBy { it.assignmentGroupId }.mapNotNull { assignmentGroupDao.findById(it.key)?.toApiModel(it.value) } + } + + suspend fun getAssignmentGroupsWithAssignmentsForGradingPeriod( + courseId: Long, + gradingPeriodId: Long + ): List { + return getAssignmentGroupsWithAssignments(courseId).map { group -> + group.copy(assignments = group.assignments.filter { it.submission?.gradingPeriodId == gradingPeriodId }) + } + } + + private suspend fun createFullApiModelFromEntity(assignmentEntity: AssignmentEntity): Assignment { + val rubricSettingEntity = assignmentEntity.rubricSettingsId?.let { rubricSettingsDao.findById(it) } + val submission = assignmentEntity.submissionId?.let { submissionFacade.getSubmissionById(it) } + val discussionTopicHeader = assignmentEntity.discussionTopicHeaderId?.let { discussionTopicHeaderFacade.getDiscussionTopicHeaderById(it) } + val lockInfo = lockInfoFacade.getLockInfoByAssignmentId(assignmentEntity.id) + val scoreStatisticsEntity = assignmentScoreStatisticsDao.findByAssignmentId(assignmentEntity.id) + val plannerOverrideEntity = assignmentEntity.plannerOverrideId?.let { plannerOverrideDao.findById(it) } + val rubricCriterionEntities = assignmentRubricCriterionDao.findByAssignmentId(assignmentEntity.id).mapNotNull { + rubricCriterionDao.findById(it.rubricId) + } + + return assignmentEntity.toApiModel( + rubric = rubricCriterionEntities.map { rubricCriterionEntity -> + val rubricCriterionRatings = rubricCriterionRatingDao.findByRubricCriterionId(rubricCriterionEntity.id).map { it.toApiModel() } + rubricCriterionEntity.toApiModel(rubricCriterionRatings) + }, + rubricSettings = rubricSettingEntity?.toApiModel(), + submission = submission, + lockInfo = lockInfo, + discussionTopicHeader = discussionTopicHeader, + scoreStatistics = scoreStatisticsEntity?.toApiModel(), + plannerOverride = plannerOverrideEntity?.toApiModel() + ).apply { + /* + * the assignment model has a submission that contains the assignment, but the inner assignment model cannot + * contain the submission because it causes a circular reference and leads to a stackoverflow exception + */ + this.submission = submission?.copy(assignment = this.copy(submission = null)) + this.discussionTopicHeader = discussionTopicHeader?.copy(assignment = this.copy(submission = null)) + } + } + + suspend fun deleteAllByCourseId(courseId: Long) { + assignmentGroupDao.deleteAllByCourseId(courseId) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ConferenceFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ConferenceFacade.kt new file mode 100644 index 0000000000..996fc8a1d3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ConferenceFacade.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.Conference +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.ConferenceDao +import com.instructure.pandautils.room.offline.daos.ConferenceRecodingDao +import com.instructure.pandautils.room.offline.entities.ConferenceEntity +import com.instructure.pandautils.room.offline.entities.ConferenceRecordingEntity + +class ConferenceFacade( + private val conferenceDao: ConferenceDao, + private val conferenceRecodingDao: ConferenceRecodingDao, + private val offlineDatabase: OfflineDatabase +) { + + suspend fun insertConferences(conferences: List, courseId: Long) { + offlineDatabase.withTransaction { + deleteAllByCourseId(courseId) + + conferenceDao.insertAll(conferences.map { ConferenceEntity(it, courseId) }) + + conferences.forEach { conference -> + conference.recordings.forEach { recording -> + conferenceRecodingDao.insert(ConferenceRecordingEntity(recording, conference.id)) + } + } + } + } + + suspend fun getConferencesByCourseId(courseId: Long): List { + return conferenceDao.findByCourseId(courseId).map { conferenceEntity -> + val recordings = conferenceRecodingDao.findByConferenceId(conferenceEntity.id).map { it.toApiModel() } + conferenceEntity.toApiModel(recordings) + } + } + + suspend fun deleteAllByCourseId(courseId: Long) { + conferenceDao.deleteAllByCourseId(courseId) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/CourseFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/CourseFacade.kt new file mode 100644 index 0000000000..6dc9b87932 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/CourseFacade.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.pandautils.room.offline.daos.* +import com.instructure.pandautils.room.offline.entities.* + +class CourseFacade( + private val termDao: TermDao, + private val courseDao: CourseDao, + private val gradingPeriodDao: GradingPeriodDao, + private val courseGradingPeriodDao: CourseGradingPeriodDao, + private val sectionDao: SectionDao, + private val tabDao: TabDao, + private val enrollmentFacade: EnrollmentFacade, + private val courseSettingsDao: CourseSettingsDao +) { + + suspend fun insertCourse(course: Course) { + course.term?.let { + termDao.insertOrUpdate(TermEntity(it)) + } + + courseDao.insertOrUpdate(CourseEntity(course)) + + course.settings?.let { + courseSettingsDao.insert(CourseSettingsEntity(it, course.id)) + } + + course.sections.forEach { section -> + sectionDao.insertOrUpdate(SectionEntity(section, course.id)) + } + + course.enrollments?.forEach { enrollment -> + enrollmentFacade.insertEnrollment(enrollment, course.id) + } + + course.gradingPeriods?.forEach { gradingPeriod -> + gradingPeriodDao.insert(GradingPeriodEntity(gradingPeriod)) + courseGradingPeriodDao.insert(CourseGradingPeriodEntity(course.id, gradingPeriod.id)) + } + + course.tabs?.forEach { tab -> + tabDao.insert(TabEntity(tab, course.id)) + } + } + + suspend fun getCourseById(id: Long): Course? { + val courseEntity = courseDao.findById(id) + return if (courseEntity != null) createFullApiModelFromEntity(courseEntity) else null + } + + suspend fun getAllCourses(): List { + return courseDao.findAll().map { + createFullApiModelFromEntity(it) + } + } + + private suspend fun createFullApiModelFromEntity(courseEntity: CourseEntity): Course { + val termEntity = courseEntity.termId?.let { termDao.findById(it) } + val enrollments = enrollmentFacade.getEnrollmentsByCourseId(courseEntity.id) + val sectionEntities = sectionDao.findByCourseId(courseEntity.id) + val courseGradingPeriodEntities = courseGradingPeriodDao.findByCourseId(courseEntity.id) + val gradingPeriods = courseGradingPeriodEntities.map { + gradingPeriodDao.findById(it.gradingPeriodId).toApiModel() + } + val tabEntities = tabDao.findByCourseId(courseEntity.id) + val settingsEntity = courseSettingsDao.findByCourseId(courseEntity.id) + + return courseEntity.toApiModel( + term = termEntity?.toApiModel(), + enrollments = enrollments.toMutableList(), + sections = sectionEntities.map { it.toApiModel() }, + gradingPeriods = gradingPeriods, + tabs = tabEntities.map { it.toApiModel() }, + settings = settingsEntity?.toApiModel() + ) + } + + suspend fun getGradingPeriodsByCourseId(id: Long): List { + val gradingPeriodEntities = courseGradingPeriodDao.findByCourseId(id).map { + gradingPeriodDao.findById(it.gradingPeriodId) + } + + return gradingPeriodEntities.map { it.toApiModel() } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicFacade.kt new file mode 100644 index 0000000000..11f725ebe4 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicFacade.kt @@ -0,0 +1,60 @@ +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionParticipant +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.pandautils.room.offline.daos.DiscussionEntryDao +import com.instructure.pandautils.room.offline.daos.DiscussionParticipantDao +import com.instructure.pandautils.room.offline.daos.DiscussionTopicDao +import com.instructure.pandautils.room.offline.entities.DiscussionEntryEntity +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicEntity + +class DiscussionTopicFacade( + private val discussionTopicDao: DiscussionTopicDao, + private val discussionParticipantDao: DiscussionParticipantDao, + private val discussionEntryDao: DiscussionEntryDao, + +) { + suspend fun insertDiscussionTopic(topicId: Long, discussionTopic: DiscussionTopic) { + val participantIds = discussionParticipantDao.upsertAll(discussionTopic.participants?.map { DiscussionParticipantEntity(it) }.orEmpty()) + val discussionEntryIds = insertDiscussionEntries(discussionTopic.views) + discussionTopicDao.insert(DiscussionTopicEntity(discussionTopic, discussionTopic.participants?.map{ it.id }.orEmpty(), discussionEntryIds, topicId)) + } + + private suspend fun insertDiscussionEntries(entries: List): List { + val authorIds = discussionParticipantDao.upsertAll(entries.mapNotNull { it.author?.let { DiscussionParticipantEntity(it) } }) + val replyIds = mutableListOf>() + entries.forEach { entry -> + entry.replies?.let { replyIds.add(insertDiscussionEntries(it)) } + } + return discussionEntryDao.insertAll(entries.mapIndexed { index, discussionEntry -> DiscussionEntryEntity(discussionEntry, replyIds[index]) }) + } + + suspend fun getDiscussionTopic(topicId: Long): DiscussionTopic? { + val topicEntity = discussionTopicDao.findById(topicId) + + val participants = mutableListOf() + topicEntity?.participantIds?.forEach { + val participant = discussionParticipantDao.findById(it) + participant?.let { participants.add(it.toApiModel()) } + } + + val views = mutableListOf() + topicEntity?.viewIds?.forEach { + val entry = getDiscussionEntries(it) + entry?.let { views.add(it) } + } + + return topicEntity?.toApiModel(participants, views) + } + + private suspend fun getDiscussionEntries(discussionEntryId: Long): DiscussionEntry? { + val view = discussionEntryDao.findById(discussionEntryId) + val author = view?.authorId?.let { discussionParticipantDao.findById(it) } + val replies = view?.replyIds?.mapNotNull { replyId -> + getDiscussionEntries(replyId) + } + return view?.toApiModel(author = author?.toApiModel(), replyDiscussionEntries = replies?.toMutableList().orEmpty().toMutableList()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt new file mode 100644 index 0000000000..ffba0e901f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/DiscussionTopicHeaderFacade.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.DiscussionParticipantDao +import com.instructure.pandautils.room.offline.daos.DiscussionTopicHeaderDao +import com.instructure.pandautils.room.offline.daos.DiscussionTopicPermissionDao +import com.instructure.pandautils.room.offline.entities.DiscussionParticipantEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicHeaderEntity +import com.instructure.pandautils.room.offline.entities.DiscussionTopicPermissionEntity + +class DiscussionTopicHeaderFacade( + private val discussionTopicHeaderDao: DiscussionTopicHeaderDao, + private val discussionParticipantDao: DiscussionParticipantDao, + private val discussionTopicPermissionDao: DiscussionTopicPermissionDao, + private val offlineDatabase: OfflineDatabase +) { + suspend fun insertDiscussion(discussionTopicHeader: DiscussionTopicHeader, courseId: Long): Long { + discussionTopicHeader.author?.let { discussionParticipantDao.insert(DiscussionParticipantEntity(it)) } + val discussionTopicHeaderId = discussionTopicHeaderDao.insert(DiscussionTopicHeaderEntity(discussionTopicHeader, courseId, null)) + val permissionId = discussionTopicHeader.permissions?.let { discussionTopicPermissionDao.insert(DiscussionTopicPermissionEntity(it, discussionTopicHeaderId)) } + discussionTopicHeaderDao.update(DiscussionTopicHeaderEntity(discussionTopicHeader.copy(id = discussionTopicHeaderId), courseId, permissionId)) + return discussionTopicHeaderId + } + + suspend fun insertDiscussions(discussionTopicHeaders: List, courseId: Long, isAnnouncement: Boolean) { + offlineDatabase.withTransaction { + discussionTopicHeaderDao.deleteAllByCourseId(courseId, isAnnouncement) + + val authors = discussionTopicHeaders + .mapNotNull { it.author } + .map { DiscussionParticipantEntity(it) } + + discussionParticipantDao.upsertAll(authors) + + val discussionEntities = + discussionTopicHeaders.mapIndexed { index, discussionTopicHeader -> + DiscussionTopicHeaderEntity( + discussionTopicHeader, + courseId, + null + ) + } + discussionTopicHeaderDao.insertAll(discussionEntities) + + val permissionIds = discussionTopicHeaders.mapIndexed { index, discussionTopicHeader -> + discussionTopicHeader.permissions?.let { + discussionTopicPermissionDao.insert(DiscussionTopicPermissionEntity(it, discussionEntities[index].id)) + } + } + + discussionEntities.forEachIndexed { index, entity -> + discussionTopicHeaderDao.update(entity.copy(permissionId = permissionIds[index])) + } + } + } + + suspend fun getDiscussionsForCourse(courseId: Long): List { + return discussionTopicHeaderDao.findAllDiscussionsForCourse(courseId) + .map { discussionTopic -> + val authorEntity = discussionTopic.authorId?.let { discussionParticipantDao.findById(it) } + val permission = discussionTopicPermissionDao.findByDiscussionTopicHeaderId(discussionTopic.id) + discussionTopic.toApiModel(author = authorEntity?.toApiModel(), permissions = permission?.toApiModel()) + } + } + + suspend fun getAnnouncementsForCourse(courseId: Long): List { + return discussionTopicHeaderDao.findAllAnnouncementsForCourse(courseId) + .map { discussionTopic -> createDiscussionApiModel(discussionTopic) } + } + + suspend fun getDiscussionTopicHeaderById(id: Long): DiscussionTopicHeader? { + val discussionTopicHeaderEntity = discussionTopicHeaderDao.findById(id) + return if (discussionTopicHeaderEntity != null) createDiscussionApiModel(discussionTopicHeaderEntity) else null + } + + suspend fun deleteAllByCourseId(courseId: Long, isAnnouncement: Boolean) { + discussionTopicHeaderDao.deleteAllByCourseId(courseId, isAnnouncement) + } + + private suspend fun createDiscussionApiModel(discussionTopicHeaderEntity: DiscussionTopicHeaderEntity): DiscussionTopicHeader { + val authorEntity = discussionTopicHeaderEntity.authorId?.let { discussionParticipantDao.findById(it) } + val permission = discussionTopicPermissionDao.findByDiscussionTopicHeaderId(discussionTopicHeaderEntity.id) + return discussionTopicHeaderEntity.toApiModel(authorEntity?.toApiModel(), permissions = permission?.toApiModel()) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/EnrollmentFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/EnrollmentFacade.kt new file mode 100644 index 0000000000..3c4ea6a328 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/EnrollmentFacade.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.pandautils.room.offline.daos.EnrollmentDao +import com.instructure.pandautils.room.offline.daos.GradesDao +import com.instructure.pandautils.room.offline.daos.UserDao +import com.instructure.pandautils.room.offline.entities.EnrollmentEntity +import com.instructure.pandautils.room.offline.entities.GradesEntity +import com.instructure.pandautils.room.offline.entities.UserEntity + +class EnrollmentFacade( + private val userDao: UserDao, + private val enrollmentDao: EnrollmentDao, + private val gradesDao: GradesDao, +) { + + suspend fun insertEnrollment(enrollment: Enrollment, courseId: Long) { + enrollment.user?.let { + userDao.insertOrUpdate(UserEntity(it)) + } + + enrollment.observedUser?.let { observedUser -> + userDao.insertOrUpdate(UserEntity(observedUser)) + } + + enrollmentDao.insertOrUpdate( + EnrollmentEntity( + enrollment, + courseId = courseId, + observedUserId = enrollment.observedUser?.id + ) + ) + + enrollment.grades?.let { grades -> + gradesDao.insert(GradesEntity(grades, enrollment.id)) + } + } + + suspend fun getEnrollmentsByCourseId(id: Long): List { + val enrollmentEntities = enrollmentDao.findByCourseId(id) + return enrollmentEntities.map { enrollmentEntity -> + val gradesEntity = gradesDao.findByEnrollmentId(enrollmentEntity.id) + val observedUserEntity = enrollmentEntity.observedUserId?.let { userDao.findById(it) } + val userEntity = userDao.findById(enrollmentEntity.userId) + + enrollmentEntity.toApiModel( + grades = gradesEntity?.toApiModel(), + observedUser = observedUserEntity?.toApiModel(), + user = userEntity?.toApiModel() + ) + } + } + + suspend fun getAllEnrollments(): List { + val enrollmentEntities = enrollmentDao.findAll() + return enrollmentEntities.map { createFullApiModelFromEntity(it) } + } + + suspend fun getEnrollmentsByGradingPeriodId(gradingPeriodId: Long): List { + val enrollmentEntities = enrollmentDao.findByGradingPeriodId(gradingPeriodId) + return enrollmentEntities.map { createFullApiModelFromEntity(it) } + } + + private suspend fun createFullApiModelFromEntity(enrollmentEntity: EnrollmentEntity): Enrollment { + val gradesEntity = gradesDao.findByEnrollmentId(enrollmentEntity.id) + val observedUserEntity = enrollmentEntity.observedUserId?.let { userDao.findById(it) } + val userEntity = userDao.findById(enrollmentEntity.userId) + + return enrollmentEntity.toApiModel( + grades = gradesEntity?.toApiModel(), + observedUser = observedUserEntity?.toApiModel(), + user = userEntity?.toApiModel() + ) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/GroupFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/GroupFacade.kt new file mode 100644 index 0000000000..ea0140801f --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/GroupFacade.kt @@ -0,0 +1,32 @@ +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.daos.GroupDao +import com.instructure.pandautils.room.offline.daos.GroupUserDao +import com.instructure.pandautils.room.offline.daos.UserDao +import com.instructure.pandautils.room.offline.entities.GroupEntity +import com.instructure.pandautils.room.offline.entities.GroupUserEntity +import com.instructure.pandautils.room.offline.entities.UserEntity + +class GroupFacade( + private val groupUserDao: GroupUserDao, + private val groupDao: GroupDao, + private val userDao: UserDao, +) { + suspend fun insertGroupWithUser(group: Group, user: User) { + groupDao.insert(GroupEntity(group)) + userDao.insert(UserEntity(user)) + groupUserDao.insert(GroupUserEntity(group.id, user.id)) + } + + suspend fun getGroupsByUserId(userId: Long): List { + val groupIds = groupUserDao.findByUserId(userId) + val groups = mutableListOf() + groupIds?.forEach { groupId -> + val group = groupDao.findById(groupId) + group?.let { groups.add(it.toApiModel()) } + } + return groups + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt new file mode 100644 index 0000000000..9e9a5d68cd --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/LockInfoFacade.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.models.LockInfo +import com.instructure.pandautils.room.offline.daos.LockInfoDao +import com.instructure.pandautils.room.offline.daos.LockedModuleDao +import com.instructure.pandautils.room.offline.daos.ModuleCompletionRequirementDao +import com.instructure.pandautils.room.offline.daos.ModuleNameDao +import com.instructure.pandautils.room.offline.entities.LockInfoEntity +import com.instructure.pandautils.room.offline.entities.LockedModuleEntity +import com.instructure.pandautils.room.offline.entities.ModuleCompletionRequirementEntity +import com.instructure.pandautils.room.offline.entities.ModuleNameEntity + +class LockInfoFacade( + private val lockInfoDao: LockInfoDao, + private val lockedModuleDao: LockedModuleDao, + private val moduleNameDao: ModuleNameDao, + private val completionRequirementDao: ModuleCompletionRequirementDao +) { + + suspend fun insertLockInfoForAssignment(lockInfo: LockInfo, assignmentId: Long) { + insertLockInfo(lockInfo, assignmentId = assignmentId) + } + + suspend fun insertLockInfoForModule(lockInfo: LockInfo, moduleId: Long) { + insertLockInfo(lockInfo, moduleId = moduleId) + } + + suspend fun insertLockInfoForPage(lockInfo: LockInfo, pageId: Long) { + insertLockInfo(lockInfo, pageId = pageId) + } + + private suspend fun insertLockInfo(lockInfo: LockInfo, assignmentId: Long? = null, moduleId: Long? = null, pageId: Long? = null) { + lockInfoDao.insert(LockInfoEntity(lockInfo, assignmentId, moduleId, pageId)) + lockInfo.contextModule?.let { lockedModule -> + lockedModuleDao.insert(LockedModuleEntity(lockedModule)) + moduleNameDao.insertAll(lockedModule.prerequisites?.map { ModuleNameEntity(it, lockedModule.id) }.orEmpty()) + lockedModule.completionRequirements.forEach { + val oldEntity = completionRequirementDao.findById(it.id) + if (oldEntity != null) { + val newEntity = oldEntity.copy(minScore = it.minScore, maxScore = it.maxScore, moduleId = lockedModule.id) + completionRequirementDao.insert(newEntity) + } else { + completionRequirementDao.insert(ModuleCompletionRequirementEntity(it, lockedModule.id)) + } + } + } + } + + suspend fun getLockInfoByAssignmentId(assignmentId: Long): LockInfo? { + val lockInfoEntity = lockInfoDao.findByAssignmentId(assignmentId) + return createFullLockInfoApiModel(lockInfoEntity) + } + + suspend fun getLockInfoByModuleId(moduleId: Long): LockInfo? { + val lockInfoEntity = lockInfoDao.findByModuleId(moduleId) + return createFullLockInfoApiModel(lockInfoEntity) + } + + suspend fun getLockInfoByPageId(pageId: Long): LockInfo? { + val lockInfoEntity = lockInfoDao.findByPageId(pageId) + return createFullLockInfoApiModel(lockInfoEntity) + } + + private suspend fun createFullLockInfoApiModel(lockInfoEntity: LockInfoEntity?): LockInfo? { + val lockedModuleEntity = lockInfoEntity?.lockedModuleId?.let { lockedModuleDao.findById(it) } + val moduleNameEntities = lockedModuleEntity?.id?.let { moduleNameDao.findByLockModuleId(it) } + val completionRequirementEntities = lockedModuleEntity?.id?.let { completionRequirementDao.findByModuleId(it) } + + return lockInfoEntity?.toApiModel( + lockedModule = lockedModuleEntity?.toApiModel( + prerequisites = moduleNameEntities?.map { it.toApiModel() }, + completionRequirements = completionRequirementEntities?.map { it.toApiModel() }.orEmpty() + ) + ) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/MasteryPathFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/MasteryPathFacade.kt new file mode 100644 index 0000000000..86ba18e837 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/MasteryPathFacade.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.models.MasteryPath +import com.instructure.pandautils.room.offline.daos.AssignmentSetDao +import com.instructure.pandautils.room.offline.daos.MasteryPathAssignmentDao +import com.instructure.pandautils.room.offline.daos.MasteryPathDao +import com.instructure.pandautils.room.offline.entities.AssignmentSetEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathAssignmentEntity +import com.instructure.pandautils.room.offline.entities.MasteryPathEntity + +class MasteryPathFacade( + private val masteryPathDao: MasteryPathDao, + private val masteryPathAssignmentDao: MasteryPathAssignmentDao, + private val assignmentSetDao: AssignmentSetDao, + private val assignmentFacade: AssignmentFacade +) { + + suspend fun insertMasteryPath(masteryPath: MasteryPath, moduleItemId: Long) { + val masteryPathEntity = MasteryPathEntity(masteryPath, moduleItemId) + masteryPathDao.insert(masteryPathEntity) + masteryPath.assignmentSets?.filterNotNull()?.forEach { assignmentSet -> + assignmentSetDao.insert(AssignmentSetEntity(assignmentSet, masteryPathEntity.id)) + assignmentSet.assignments.forEach { masteryPathAssignment -> + masteryPathAssignmentDao.insert(MasteryPathAssignmentEntity(masteryPathAssignment)) + masteryPathAssignment.model?.let { assignment -> + assignmentFacade.insertAssignment(assignment) + } + } + } + } + + suspend fun getMasteryPath(moduleItemId: Long): MasteryPath? { + val masteryPath = masteryPathDao.findById(moduleItemId) + + val assignmentSets = masteryPath?.let { + val assignmentSets = assignmentSetDao.findByMasteryPathId(it.id) + assignmentSets.map { assignmentSet -> + val masteryPathAssignments = masteryPathAssignmentDao.findByAssignmentSetId(assignmentSet.id) + .map { masteryPathAssignment -> + val assignment = assignmentFacade.getAssignmentById(masteryPathAssignment.assignmentId) + masteryPathAssignment.toApiModel(assignment) + } + assignmentSet.toApiModel(masteryPathAssignments) + } + }.orEmpty() + + return masteryPath?.toApiModel(assignmentSets) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ModuleFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ModuleFacade.kt new file mode 100644 index 0000000000..705ad95932 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ModuleFacade.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.ModuleItem +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.ModuleCompletionRequirementDao +import com.instructure.pandautils.room.offline.daos.ModuleContentDetailsDao +import com.instructure.pandautils.room.offline.daos.ModuleItemDao +import com.instructure.pandautils.room.offline.daos.ModuleObjectDao +import com.instructure.pandautils.room.offline.entities.ModuleCompletionRequirementEntity +import com.instructure.pandautils.room.offline.entities.ModuleContentDetailsEntity +import com.instructure.pandautils.room.offline.entities.ModuleItemEntity +import com.instructure.pandautils.room.offline.entities.ModuleObjectEntity + +class ModuleFacade( + private val moduleObjectDao: ModuleObjectDao, + private val moduleItemDao: ModuleItemDao, + private val completionRequirementDao: ModuleCompletionRequirementDao, + private val moduleContentDetailsDao: ModuleContentDetailsDao, + private val lockInfoFacade: LockInfoFacade, + private val masteryPathFacade: MasteryPathFacade, + private val offlineDatabase: OfflineDatabase +) { + + suspend fun insertModules(moduleObjects: List, courseId: Long) { + offlineDatabase.withTransaction { + deleteAllByCourseId(courseId) + + moduleObjects.forEach { moduleObject -> + moduleObjectDao.insert(ModuleObjectEntity(moduleObject, courseId)) + moduleObject.items.forEach { moduleItem -> + val moduleItemEntity = ModuleItemEntity(moduleItem, moduleObject.id) + moduleItemDao.insert(moduleItemEntity) + + moduleItem.completionRequirement?.let { + completionRequirementDao.insert(ModuleCompletionRequirementEntity(it, moduleItemEntity.moduleId, moduleItemEntity.id)) + } + + moduleItem.moduleDetails?.let { moduleDetails -> + moduleContentDetailsDao.insert(ModuleContentDetailsEntity(moduleDetails, moduleItemEntity.id)) + moduleDetails.lockInfo?.let { lockInfo -> + lockInfoFacade.insertLockInfoForModule(lockInfo, moduleItemEntity.id) + } + } + + moduleItem.masteryPaths?.let { masteryPaths -> + masteryPathFacade.insertMasteryPath(masteryPaths, moduleItemEntity.id) + } + } + } + } + } + + suspend fun getModuleObjects(courseId: Long): List { + val moduleObjects = moduleObjectDao.findByCourseId(courseId) + return moduleObjects.map { moduleObjectEntity -> createModuleObjectApiModel(moduleObjectEntity) } + } + + suspend fun getModuleObjectById(moduleId: Long): ModuleObject? { + val moduleObjectEntity = moduleObjectDao.findById(moduleId) + return moduleObjectEntity?.let { createModuleObjectApiModel(it) } + } + + private suspend fun createModuleObjectApiModel(moduleObjectEntity: ModuleObjectEntity): ModuleObject { + val moduleItems = moduleItemDao.findByModuleId(moduleObjectEntity.id).map { createModuleItemApiModel(it) } + return moduleObjectEntity.toApiModel(moduleItems) + } + + suspend fun getModuleItems(moduleId: Long): List { + val moduleItemEntities = moduleItemDao.findByModuleId(moduleId) + return moduleItemEntities.map { moduleItemEntity -> createModuleItemApiModel(moduleItemEntity) } + } + + private suspend fun createModuleItemApiModel(moduleItemEntity: ModuleItemEntity): ModuleItem { + val completionRequirement = completionRequirementDao.findById(moduleItemEntity.id)?.toApiModel() + val lockInfo = lockInfoFacade.getLockInfoByModuleId(moduleItemEntity.id) + val moduleContentDetails = moduleContentDetailsDao.findById(moduleItemEntity.id)?.toApiModel(lockInfo) + val masteryPath = masteryPathFacade.getMasteryPath(moduleItemEntity.id) + return moduleItemEntity.toApiModel(completionRequirement, moduleContentDetails, masteryPath) + } + + suspend fun getModuleItemById(moduleItemId: Long): ModuleItem? { + val moduleItemEntity = moduleItemDao.findById(moduleItemId) + return moduleItemEntity?.let { createModuleItemApiModel(it) } + } + + suspend fun getModuleItemByAssetIdAndType(assetType: String, assetId: Long): ModuleItem? { + val moduleItemEntity = moduleItemDao.findByTypeAndContentId(assetType, assetId) + return moduleItemEntity?.let { createModuleItemApiModel(it) } + } + + suspend fun getModuleItemForPage(pageUrl: String): ModuleItem? { + val moduleItemEntity = moduleItemDao.findByPageUrl(pageUrl) + return moduleItemEntity?.let { createModuleItemApiModel(it) } + } + + suspend fun deleteAllByCourseId(courseId: Long) { + moduleObjectDao.deleteAllByCourseId(courseId) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/PageFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/PageFacade.kt new file mode 100644 index 0000000000..504f33ce9e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/PageFacade.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.Page +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.PageDao +import com.instructure.pandautils.room.offline.entities.PageEntity + +class PageFacade( + private val pageDao: PageDao, + private val lockedInfoFacade: LockInfoFacade, + private val offlineDatabase: OfflineDatabase +) { + suspend fun insertPages(pages: List, courseId: Long) { + offlineDatabase.withTransaction { + deleteAllByCourseId(courseId) + pageDao.insertAll(pages.map { PageEntity(it, courseId) }) + pages.forEach { page -> + page.lockInfo?.let { + lockedInfoFacade.insertLockInfoForPage(it, page.id) + } + } + } + } + + suspend fun insertPage(page: Page, courseId: Long) { + offlineDatabase.withTransaction { + pageDao.insert(PageEntity(page, courseId)) + page.lockInfo?.let { + lockedInfoFacade.insertLockInfoForPage(it, page.id) + } + } + } + + suspend fun getFrontPage(courseId: Long): Page? { + return pageDao.getFrontPage(courseId)?.let { createFullApiModel(it) } + } + + suspend fun findByCourseId(courseId: Long): List { + return pageDao.findByCourseId(courseId).map { createFullApiModel(it) } + } + + suspend fun getPageDetails(courseId: Long, pageId: String): Page? { + return pageDao.getPageDetails(courseId, pageId)?.let { createFullApiModel(it) } + } + + suspend fun deleteAllByCourseId(courseId: Long) { + pageDao.deleteAllByCourseId(courseId) + } + + private suspend fun createFullApiModel(pageEntity: PageEntity): Page { + val lockInfo = lockedInfoFacade.getLockInfoByPageId(pageEntity.id) + return pageEntity.toApiModel(lockInfo) + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ScheduleItemFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ScheduleItemFacade.kt new file mode 100644 index 0000000000..15c0c3b271 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/ScheduleItemFacade.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.ScheduleItem +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.AssignmentDao +import com.instructure.pandautils.room.offline.daos.AssignmentOverrideDao +import com.instructure.pandautils.room.offline.daos.ScheduleItemAssignmentOverrideDao +import com.instructure.pandautils.room.offline.daos.ScheduleItemDao +import com.instructure.pandautils.room.offline.entities.AssignmentOverrideEntity +import com.instructure.pandautils.room.offline.entities.ScheduleItemAssignmentOverrideEntity +import com.instructure.pandautils.room.offline.entities.ScheduleItemEntity + +class ScheduleItemFacade( + private val scheduleItemDao: ScheduleItemDao, + private val assignmentOverrideDao: AssignmentOverrideDao, + private val scheduleItemAssignmentOverrideDao: ScheduleItemAssignmentOverrideDao, + private val assignmentDao: AssignmentDao, + private val offlineDatabase: OfflineDatabase +) { + suspend fun insertScheduleItems(scheduleItems: List, courseId: Long) { + offlineDatabase.withTransaction { + deleteAllByCourseId(courseId) + + scheduleItems.forEach { scheduleItem -> + scheduleItemDao.insert(ScheduleItemEntity(scheduleItem, courseId)) + + scheduleItem.assignmentOverrides?.let { assignmentOverrides -> + assignmentOverrides.forEach { assignmentOverride -> + assignmentOverride?.let { + assignmentOverrideDao.insert(AssignmentOverrideEntity(it)) + scheduleItemAssignmentOverrideDao.insert( + ScheduleItemAssignmentOverrideEntity( + it.id, + scheduleItem.itemId + ) + ) + } + } + } + } + } + } + + suspend fun findByItemType(contextCodes: List, itemType: String): List { + val entities = scheduleItemDao.findByItemType(contextCodes, itemType) + return entities.map { scheduleItemEntity -> + val assignment = scheduleItemEntity.assignmentId?.let { assignmentId -> assignmentDao.findById(assignmentId)?.toApiModel() } + val assignmentOverrideIds = scheduleItemAssignmentOverrideDao.findByScheduleItemId(scheduleItemEntity.id).map { it.assignmentOverrideId } + val assignmentOverrides = assignmentOverrideDao.findByIds(assignmentOverrideIds).map { it.toApiModel() } + scheduleItemEntity.toApiModel(assignmentOverrides, assignment) + } + } + + suspend fun deleteAllByCourseId(courseId: Long) { + scheduleItemDao.deleteAllByCourseId(courseId) + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt new file mode 100644 index 0000000000..971e76e94c --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SubmissionFacade.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Submission +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.daos.* +import com.instructure.pandautils.room.offline.entities.* + +class SubmissionFacade( + private val submissionDao: SubmissionDao, + private val groupDao: GroupDao, + private val mediaCommentDao: MediaCommentDao, + private val userDao: UserDao, + private val submissionCommentDao: SubmissionCommentDao, + private val attachmentDao: AttachmentDao, + private val authorDao: AuthorDao, + private val rubricCriterionAssessmentDao: RubricCriterionAssessmentDao +) { + + suspend fun insertSubmission(submission: Submission) { + submission.group?.let { group -> groupDao.insertOrUpdate(GroupEntity(group)) } + + submissionDao.insertOrUpdate(SubmissionEntity(submission, submission.group?.id, submission.mediaComment?.mediaId)) + + submission.mediaComment?.let { mediaComment -> + mediaCommentDao.insert(MediaCommentEntity(mediaComment, submission.id, submission.attempt)) + } + + submission.user?.let { + userDao.insertOrUpdate(UserEntity(it)) + } + + submission.submissionComments.forEach { submissionComment -> + submissionCommentDao.insert(SubmissionCommentEntity(submissionComment, submission.id, submission.attempt)) + + submissionComment.mediaComment?.let { + mediaCommentDao.insert(MediaCommentEntity(it, submission.id, submission.attempt)) + } + + submissionComment.attachments.map { + attachmentDao.insert(AttachmentEntity(it, submissionId = submission.id, submissionCommentId = submissionComment.id)) + } + + submissionComment.author?.let { + authorDao.insert(AuthorEntity(it)) + } + } + + attachmentDao.insertAll(submission.attachments.map { + AttachmentEntity(it, submissionId = submission.id, attempt = submission.attempt) + }) + + rubricCriterionAssessmentDao.insertAll(submission.rubricAssessment.map { + RubricCriterionAssessmentEntity(it.value, it.key, submission.assignmentId) + }) + + submission.submissionHistory.forEach { submissionHistoryItem -> + submissionHistoryItem?.let { insertSubmission(it) } + } + } + + suspend fun getSubmissionById(id: Long): Submission? { + val submissionHistoryEntities = submissionDao.findById(id) + return submissionHistoryEntities.lastOrNull()?.let { submissionEntity -> + createApiModelFromEntity(submissionEntity).copy(submissionHistory = submissionHistoryEntities.map { + createApiModelFromEntity(it) + }) + } + } + + private suspend fun createApiModelFromEntity(submissionEntity: SubmissionEntity): Submission { + val mediaCommentEntity = mediaCommentDao.findById(submissionEntity.mediaCommentId) + val userEntity = submissionEntity.userId?.let { userDao.findById(it) } + val groupEntity = submissionEntity.groupId?.let { groupDao.findById(it) } + val submissionCommentEntities = submissionCommentDao.findBySubmissionId(submissionEntity.id) + val attachmentEntities = attachmentDao.findBySubmissionId(submissionEntity.id) + val rubricCriterionAssessmentEntities = rubricCriterionAssessmentDao.findByAssignmentId(submissionEntity.assignmentId) + + return submissionEntity.toApiModel( + mediaComment = mediaCommentEntity?.toApiModel(), + user = userEntity?.toApiModel(), + group = groupEntity?.toApiModel(), + submissionComments = submissionCommentEntities.map { it.toApiModel() }, + attachments = attachmentEntities.filter { it.attempt == submissionEntity.attempt }.map { it.toApiModel() }, + rubricAssessment = HashMap(rubricCriterionAssessmentEntities.associateBy({ it.id }, { it.toApiModel() })) + ) + } + + suspend fun findByAssignmentIds(assignmentIds: List): List { + val submissionsByAssignmentIds = submissionDao.findByAssignmentIds(assignmentIds) + return submissionsByAssignmentIds.mapNotNull { getSubmissionById(it.id) } + } + + suspend fun findByAssignmentId(assignmentId: Long): Submission? { + return submissionDao.findByAssignmentId(assignmentId)?.let { + getSubmissionById(it.id) + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SyncSettingsFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SyncSettingsFacade.kt new file mode 100644 index 0000000000..7e7196a1c3 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/SyncSettingsFacade.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import androidx.lifecycle.LiveData +import com.instructure.pandautils.features.offline.sync.settings.SyncFrequency +import com.instructure.pandautils.room.offline.daos.SyncSettingsDao +import com.instructure.pandautils.room.offline.entities.SyncSettingsEntity + +class SyncSettingsFacade(private val syncSettingsDao: SyncSettingsDao) { + + suspend fun getSyncSettingsListenable(): LiveData { + val settings = syncSettingsDao.findSyncSettings() + if (settings == null) createDefault() + return syncSettingsDao.findSyncSettingsLiveData() + } + + suspend fun getSyncSettings(): SyncSettingsEntity { + return syncSettingsDao.findSyncSettings() ?: createDefault() + } + + suspend fun update(syncSettingsEntity: SyncSettingsEntity) { + syncSettingsDao.update(syncSettingsEntity) + } + + private suspend fun createDefault(): SyncSettingsEntity { + val default = SyncSettingsEntity( + autoSyncEnabled = true, + syncFrequency = SyncFrequency.DAILY, + wifiOnly = true + ) + + syncSettingsDao.insert(default) + + return default + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/UserFacade.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/UserFacade.kt new file mode 100644 index 0000000000..7f30a73135 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/facade/UserFacade.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.room.offline.facade + +import androidx.room.withTransaction +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.room.offline.OfflineDatabase +import com.instructure.pandautils.room.offline.daos.EnrollmentDao +import com.instructure.pandautils.room.offline.daos.SectionDao +import com.instructure.pandautils.room.offline.daos.UserDao +import com.instructure.pandautils.room.offline.entities.EnrollmentEntity + +class UserFacade( + private val userDao: UserDao, + private val enrollmentDao: EnrollmentDao, + private val sectionDao: SectionDao, + private val enrollmentFacade: EnrollmentFacade, + private val offlineDatabase: OfflineDatabase +) { + suspend fun insertUsers(userList: List, courseId: Long) { + offlineDatabase.withTransaction { + val courseSectionIds = sectionDao.findByCourseId(courseId).map { it.id } + userList.forEach { user -> + user.enrollments.forEach { enrollment -> + val hasSection = courseSectionIds.contains(enrollment.courseSectionId) + enrollmentFacade.insertEnrollment( + enrollment.copy( + user = user, + courseSectionId = enrollment.courseSectionId.takeIf { hasSection } ?: 0 + ), courseId + ) + } + } + } + } + + suspend fun getUsersByCourseId(courseId: Long): List { + val enrollments = enrollmentDao.findByCourseId(courseId) + return getUsersFromEnrollment(enrollments) + } + + suspend fun getUsersByCourseIdAndRole(courseId: Long, role: Enrollment.EnrollmentType): List { + val enrollments = enrollmentDao.findByCourseIdAndRole(courseId, role.name) + return getUsersFromEnrollment(enrollments) + } + + private suspend fun getUsersFromEnrollment(enrollments: List): List { + return enrollments.groupBy { it.userId }.keys.mapNotNull { userId -> + userDao.findById(userId)?.toApiModel(enrollments.map { it.toApiModel() }.filter { it.userId == userId }) + } + } + + suspend fun getUserById(userId: Long): User? { + return enrollmentDao.findByUserId(userId)?.let { + getUsersFromEnrollment(listOf(it)).firstOrNull() + } + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/model/CourseSyncSettingsWithFiles.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/model/CourseSyncSettingsWithFiles.kt new file mode 100644 index 0000000000..3f991ab639 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/model/CourseSyncSettingsWithFiles.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package com.instructure.pandautils.room.offline.model + +import androidx.room.Embedded +import androidx.room.Relation +import com.instructure.pandautils.room.offline.entities.CourseSyncSettingsEntity +import com.instructure.pandautils.room.offline.entities.FileSyncSettingsEntity + +data class CourseSyncSettingsWithFiles( + @Embedded + val courseSyncSettings: CourseSyncSettingsEntity, + + @Relation( + entity = FileSyncSettingsEntity::class, + parentColumn = "courseId", + entityColumn = "courseId" + ) + val files: List +) { + fun isFileSelected(fileId: Long): Boolean { + return courseSyncSettings.fullFileSync || files.find { it.id == fileId } != null + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/model/SubmissionCommentWithAttachments.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/model/SubmissionCommentWithAttachments.kt similarity index 77% rename from libs/pandautils/src/main/java/com/instructure/pandautils/room/common/model/SubmissionCommentWithAttachments.kt rename to libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/model/SubmissionCommentWithAttachments.kt index 618ee58b54..3cc23d8df4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/room/common/model/SubmissionCommentWithAttachments.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/room/offline/model/SubmissionCommentWithAttachments.kt @@ -1,12 +1,12 @@ -package com.instructure.pandautils.room.common.model +package com.instructure.pandautils.room.offline.model import androidx.room.Embedded import androidx.room.Relation import com.instructure.canvasapi2.models.SubmissionComment -import com.instructure.pandautils.room.common.entities.AttachmentEntity -import com.instructure.pandautils.room.common.entities.AuthorEntity -import com.instructure.pandautils.room.common.entities.MediaCommentEntity -import com.instructure.pandautils.room.common.entities.SubmissionCommentEntity +import com.instructure.pandautils.room.offline.entities.AttachmentEntity +import com.instructure.pandautils.room.offline.entities.AuthorEntity +import com.instructure.pandautils.room.offline.entities.MediaCommentEntity +import com.instructure.pandautils.room.offline.entities.SubmissionCommentEntity data class SubmissionCommentWithAttachments( @Embedded diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt index 3bdf5c2706..22a38fbab0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Const.kt @@ -45,6 +45,7 @@ object Const { const val DISCUSSION_ID = "discussion_id" const val FEATURE_NAME = "featureName" const val FILE_URL = "fileUrl" + const val FILE_ID = "fileId" const val FILE_DOWNLOADED = "fileDownloaded" const val FOLDER = "folder" const val FOLDER_ID = "folderId" diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Extensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Extensions.kt index af5f6fd59e..c16d98d61c 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Extensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/Extensions.kt @@ -16,6 +16,8 @@ package com.instructure.pandautils.utils +import androidx.work.Data +import androidx.work.WorkInfo import com.google.gson.Gson import java.util.* import kotlin.math.ln @@ -52,3 +54,8 @@ fun Boolean?.orDefault(default: Boolean = false): Boolean { fun Double?.orDefault(default: Double = 0.0): Double { return this ?: default } + +fun Data.newBuilder(): Data.Builder { + return Data.Builder() + .putAll(this) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt index a87dda717e..4cf2e6d2f3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt @@ -24,6 +24,7 @@ import com.instructure.pandautils.BuildConfig import com.instructure.pandautils.room.appdatabase.daos.EnvironmentFeatureFlagsDao import com.instructure.pandautils.room.appdatabase.entities.EnvironmentFeatureFlags +const val FEATURE_FLAG_OFFLINE = "mobile_offline_mode" class FeatureFlagProvider( private val userManager: UserManager, private val apiPrefs: ApiPrefs, @@ -42,19 +43,23 @@ class FeatureFlagProvider( } } - fun getDiscussionRedesignFeatureFlag(): Boolean { - return BuildConfig.IS_DEBUG + suspend fun getDiscussionRedesignFeatureFlag(): Boolean { + return BuildConfig.IS_DEBUG && checkEnvironmentFeatureFlag("react_discussions_post") } suspend fun fetchEnvironmentFeatureFlags() { - val restParams = RestParams(isForceReadFromNetwork = false) + val restParams = RestParams(isForceReadFromNetwork = true, shouldIgnoreToken = true) val featureFlags = featuresApi.getEnvironmentFeatureFlags(restParams).dataOrNull ?: return apiPrefs.user?.id?.let { environmentFeatureFlags.insert(EnvironmentFeatureFlags(it, featureFlags)) } } - suspend fun checkEnvironmentFeatureFlag(featureFlag: String): Boolean { + suspend fun offlineEnabled(): Boolean { + return checkEnvironmentFeatureFlag(FEATURE_FLAG_OFFLINE) && !apiPrefs.canvasForElementary + } + + private suspend fun checkEnvironmentFeatureFlag(featureFlag: String): Boolean { return apiPrefs.user?.id?.let { environmentFeatureFlags.findByUserId(it)?.featureFlags?.get(featureFlag) == true } ?: false } } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileUploadUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileUploadUtils.kt index 10cef14153..7995457d75 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileUploadUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileUploadUtils.kt @@ -43,6 +43,12 @@ object FileUploadUtils { private const val FILE_SCHEME = "file" private const val CONTENT_SCHEME = "content" + private val APPLE_EXTENSIONS_MIME_TYPES = mapOf ( + "pages" to "application/vnd.apple.pages", + "numbers" to "application/vnd.apple.numbers", + "key" to "application/vnd.apple.keynote", + ) + /** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and @@ -267,10 +273,16 @@ object FileUploadUtils { var mimeType: String? = null if (FILE_SCHEME.equals(scheme, ignoreCase = true)) { if (uri.lastPathSegment != null) { - mimeType = getMimeTypeFromFileNameWithExtension(uri.lastPathSegment!!) + val extension = uri.lastPathSegment!! + mimeType = getMimeTypeFromFileNameWithExtension(extension) } } else if (CONTENT_SCHEME.equals(scheme, ignoreCase = true)) { mimeType = resolver.getType(uri) + val fileName = getFileNameFromUri(resolver, uri) + val extension = fileName?.substringAfterLast('.')?.substringBefore(' ') + if (APPLE_EXTENSIONS_MIME_TYPES.keys.contains(extension)) { + mimeType = APPLE_EXTENSIONS_MIME_TYPES[extension] + } } return mimeType ?: "*/*" } @@ -282,7 +294,12 @@ object FileUploadUtils { if (index != -1) { ext = fileNameWithExtension.substring(index + 1).lowercase(Locale.getDefault()) // Add one so the dot isn't included } - return mime.getMimeTypeFromExtension(ext).orEmpty() + var mimeType = mime.getMimeTypeFromExtension(ext).orEmpty() + val extension = fileNameWithExtension.substringAfterLast('.') + if (APPLE_EXTENSIONS_MIME_TYPES.keys.contains(extension)) { + mimeType = APPLE_EXTENSIONS_MIME_TYPES[extension].orEmpty() + } + return mimeType } private fun getTempFilename(fileName: String?): String { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt index fc914c0438..fc218671f4 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/HtmlContentFormatter.kt @@ -20,7 +20,7 @@ import android.content.Context import com.google.firebase.crashlytics.FirebaseCrashlytics import com.instructure.canvasapi2.managers.OAuthManager import com.instructure.canvasapi2.models.AuthenticatedSession -import com.instructure.canvasapi2.utils.weave.awaitApi +import com.instructure.canvasapi2.utils.weave.apiAsync import com.instructure.pandautils.R import com.instructure.pandautils.discussions.DiscussionHtmlTemplates import com.instructure.pandautils.views.CanvasWebView @@ -113,7 +113,12 @@ class HtmlContentFormatter( } private suspend fun authenticateLTIUrl(ltiUrl: String): String { - return awaitApi { oAuthManager.getAuthenticatedSession(ltiUrl, it) }.sessionUrl + val ltiResult = apiAsync { oAuthManager.getAuthenticatedSession(ltiUrl, it) }.await() + return if (ltiResult.isSuccess) { + return ltiResult.dataOrNull?.sessionUrl ?: ltiUrl + } else { + ltiUrl + } } private fun iframeWithLink(srcUrl: String, iframe: String, context: Context): String { diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.kt new file mode 100644 index 0000000000..d5f7016a6e --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/NetworkStateProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.pandautils.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + + +interface NetworkStateProvider { + val isOnlineLiveData: LiveData + fun isOnline(): Boolean +} + +class NetworkStateProviderImpl(context: Context) : NetworkStateProvider { + + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val _isOnlineLiveData = MutableLiveData() + + override val isOnlineLiveData: LiveData + get() = _isOnlineLiveData + + init { + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + val hasActiveNetwork = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).orDefault() + _isOnlineLiveData.postValue(hasActiveNetwork) + + connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + _isOnlineLiveData.postValue(true) + } + + override fun onLost(network: Network) { + super.onLost(network) + _isOnlineLiveData.postValue(false) + } + }) + } + + override fun isOnline(): Boolean { + return _isOnlineLiveData.value.orDefault() + } +} \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StorageUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StorageUtils.kt new file mode 100644 index 0000000000..741f61b0ac --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/StorageUtils.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.utils + +import android.content.Context +import android.os.Environment +import java.io.File + +class StorageUtils(private val context: Context) { + + fun getTotalSpace(): Long { + val externalStorageDirectory = Environment.getExternalStorageDirectory() + return externalStorageDirectory.totalSpace + } + + fun getFreeSpace(): Long { + val externalStorageDirectory = Environment.getExternalStorageDirectory() + return externalStorageDirectory.freeSpace + } + + fun getAppSize(): Long { + val appInfo = context.applicationInfo + val appSize = File(appInfo.publicSourceDir).length() + val dataDirSize = getDirSize(File(appInfo.dataDir)) + + return appSize + dataDirSize + } + + private fun getDirSize(directory: File?): Long { + if (directory == null || !directory.isDirectory) return 0 + var size: Long = 0 + val files = directory.listFiles() ?: return 0 + for (file in files) size += if (file.isDirectory) getDirSize(file) else file.length() + return size + } +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt index 6a1d40657a..d102f15741 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt @@ -198,12 +198,15 @@ object ViewStyler { imageView.setImageDrawable(drawable) } - fun makeColorStateList(defaultColor: Int, brand: Int) = generateColorStateList( - intArrayOf(-android.R.attr.state_enabled) to defaultColor, + @JvmOverloads + fun makeColorStateList(defaultColor: Int, brand: Int, disabledColor: Int = defaultColor) = generateColorStateList( + intArrayOf(-android.R.attr.state_enabled) to disabledColor, intArrayOf(android.R.attr.state_focused, -android.R.attr.state_pressed) to brand, intArrayOf(android.R.attr.state_focused, android.R.attr.state_pressed) to brand, intArrayOf(-android.R.attr.state_focused, android.R.attr.state_pressed) to brand, intArrayOf(android.R.attr.state_checked) to brand, + intArrayOf(com.google.android.material.R.attr.state_indeterminate) to brand, + intArrayOf() to defaultColor ) @@ -261,7 +264,8 @@ fun AlertDialog.Builder.showThemed() { } fun BottomNavigationView.applyTheme(@ColorInt selectedColor: Int = ThemePrefs.brandColor, @ColorInt unselectedColor: Int) { - val colorStateList = ViewStyler.makeColorStateList(unselectedColor, selectedColor) + val disabledColor = ThemePrefs.increaseAlpha(unselectedColor, 128) + val colorStateList = ViewStyler.makeColorStateList(unselectedColor, selectedColor, disabledColor) this.itemIconTintList = colorStateList this.itemTextColor = colorStateList } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt index 4b006a4e35..96730a03c6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/views/CanvasWebView.kt @@ -178,6 +178,7 @@ class CanvasWebView @JvmOverloads constructor( this.settings.useWideViewPort = true this.webViewClient = CanvasWebViewClient() this.settings.domStorageEnabled = true + this.settings.allowFileAccess = true this.settings.mediaPlaybackRequiresUserGesture = false // Disabled to allow videos to be played // Increase text size based on the devices accessibility setting @@ -565,7 +566,6 @@ class CanvasWebView @JvmOverloads constructor( } fun setCanvasWebChromeClientShowFilePickerCallback(callback: VideoPickerCallback?) { - this.settings.allowFileAccess = true videoPickerCallback = callback } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/views/EmptyView.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/views/EmptyView.kt index ec26bb77be..503c3c682d 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/views/EmptyView.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/views/EmptyView.kt @@ -60,6 +60,15 @@ class EmptyView @JvmOverloads constructor( loading.root.visibility = View.VISIBLE } + override fun setLoadingWithAnimation(titleRes: Int, messageRes: Int, animationRes: Int): Unit = with(binding) { + setTitleText(titleRes) + setMessageText(messageRes) + animationView.setAnimation(animationRes) + title.setVisible() + message.setVisible() + animationView.setVisible() + } + override fun setDisplayNoConnection(isNoConnection: Boolean) { isDisplayNoConnection = isNoConnection } diff --git a/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml b/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml index d6ee277edd..f5d7aafd76 100644 --- a/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml +++ b/libs/pandautils/src/main/res/drawable/bg_rounded_rectangle.xml @@ -1,5 +1,5 @@ + + + + + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/drawable/storage_progress_bar_background.xml b/libs/pandautils/src/main/res/drawable/storage_progress_bar_background.xml new file mode 100644 index 0000000000..a4a4b5decc --- /dev/null +++ b/libs/pandautils/src/main/res/drawable/storage_progress_bar_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/pandautils/src/main/res/layout/empty_view.xml b/libs/pandautils/src/main/res/layout/empty_view.xml index ca88d94b2c..3ddfeddf87 100644 --- a/libs/pandautils/src/main/res/layout/empty_view.xml +++ b/libs/pandautils/src/main/res/layout/empty_view.xml @@ -57,7 +57,6 @@ android:orientation="vertical" app:layout_constraintGuide_percent="@dimen/text_right"/> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +