diff --git a/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg b/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg new file mode 100644 index 0000000000..ab80d41cfa --- /dev/null +++ b/apps/flutter_parent/assets/svg/canvas-parent-login-logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg b/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg index a6413426ae..b595560d0c 100644 --- a/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg +++ b/apps/flutter_parent/assets/svg/canvas-parent-login-logo.svg @@ -1,10 +1,10 @@ - - - - - - - + + + + + + + diff --git a/apps/flutter_parent/flutter_parent_sdk_url b/apps/flutter_parent/flutter_parent_sdk_url new file mode 100644 index 0000000000..6fcfede9b9 --- /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_2.5.3-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 26fa996e0b..4872b5fe70 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -1705,4 +1705,7 @@ class AppLocalizations { String get aboutVersionTitle => Intl.message('Version', desc: 'Title for Version field on about page'); + + String get aboutLogoSemanticsLabel => + Intl.message('Instructure logo', desc: 'Semantics label for the Instructure logo on the about page'); } diff --git a/apps/flutter_parent/lib/l10n/res/intl_en.arb b/apps/flutter_parent/lib/l10n/res/intl_en.arb index 9d2ca6f103..d5edbf3ece 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_en.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_en.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure logo", + "@Instructure logo": { + "description": "Semantics label for the Instructure logo on the about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/l10n/res/intl_messages.arb b/apps/flutter_parent/lib/l10n/res/intl_messages.arb index 9d2ca6f103..d5edbf3ece 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_messages.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:46.988317", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Alerts", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -2742,5 +2742,12 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Instructure logo": "Instructure logo", + "@Instructure logo": { + "description": "Semantics label for the Instructure logo on the about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/models/course.dart b/apps/flutter_parent/lib/models/course.dart index 4d9036b03d..931a1bc5b0 100644 --- a/apps/flutter_parent/lib/models/course.dart +++ b/apps/flutter_parent/lib/models/course.dart @@ -15,8 +15,10 @@ library course; 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:flutter_parent/models/course_settings.dart'; +import 'package:flutter_parent/models/grading_scheme_item.dart'; import 'package:flutter_parent/models/section.dart'; import 'package:flutter_parent/models/term.dart'; @@ -127,6 +129,16 @@ abstract class Course implements Built { @nullable CourseSettings get settings; + @nullable + @BuiltValueField(wireName: 'grading_scheme') + 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)); + } + static void _initializeBuilder(CourseBuilder b) => b ..id = '' ..enrollments = ListBuilder() @@ -176,6 +188,12 @@ abstract class Course implements Built { 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; + } } @BuiltValueEnum(wireName: 'default_view') diff --git a/apps/flutter_parent/lib/models/course.g.dart b/apps/flutter_parent/lib/models/course.g.dart index 3c8a4f7d71..737bb7026e 100644 --- a/apps/flutter_parent/lib/models/course.g.dart +++ b/apps/flutter_parent/lib/models/course.g.dart @@ -154,6 +154,13 @@ class _$CourseSerializer implements StructuredSerializer { ..add('settings') ..add(serializers.serialize(value, specifiedType: const FullType(CourseSettings))); + value = object.gradingScheme; + + result + ..add('grading_scheme') + ..add(serializers.serialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(JsonObject)]))); return result; } @@ -265,6 +272,12 @@ class _$CourseSerializer implements StructuredSerializer { result.settings.replace(serializers.deserialize(value, 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); + break; } } @@ -344,6 +357,8 @@ class _$Course extends Course { final BuiltList
sections; @override final CourseSettings settings; + @override + final BuiltList gradingScheme; factory _$Course([void Function(CourseBuilder) updates]) => (new CourseBuilder()..update(updates)).build(); @@ -375,7 +390,8 @@ class _$Course extends Course { this.homePage, this.term, this.sections, - this.settings}) + this.settings, + this.gradingScheme}) : super._() { BuiltValueNullFieldError.checkNotNull(id, 'Course', 'id'); BuiltValueNullFieldError.checkNotNull(name, 'Course', 'name'); @@ -436,7 +452,8 @@ class _$Course extends Course { homePage == other.homePage && term == other.term && sections == other.sections && - settings == other.settings; + settings == other.settings && + gradingScheme == other.gradingScheme; } @override @@ -459,26 +476,26 @@ class _$Course extends Course { $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)); + $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)); } @override @@ -511,7 +528,8 @@ class _$Course extends Course { ..add('homePage', homePage) ..add('term', term) ..add('sections', sections) - ..add('settings', settings)) + ..add('settings', settings) + ..add('gradingScheme', gradingScheme)) .toString(); } } @@ -642,6 +660,12 @@ class CourseBuilder implements Builder { _$this._settings ??= new CourseSettingsBuilder(); set settings(CourseSettingsBuilder settings) => _$this._settings = settings; + ListBuilder _gradingScheme; + ListBuilder get gradingScheme => + _$this._gradingScheme ??= new ListBuilder(); + set gradingScheme(ListBuilder gradingScheme) => + _$this._gradingScheme = gradingScheme; + CourseBuilder() { Course._initializeBuilder(this); } @@ -676,6 +700,7 @@ class CourseBuilder implements Builder { _term = $v.term?.toBuilder(); _sections = $v.sections?.toBuilder(); _settings = $v.settings?.toBuilder(); + _gradingScheme = $v.gradingScheme?.toBuilder(); _$v = null; } return this; @@ -733,7 +758,8 @@ class CourseBuilder implements Builder { homePage: homePage, term: _term?.build(), sections: _sections?.build(), - settings: _settings?.build()); + settings: _settings?.build(), + gradingScheme: _gradingScheme?.build()); } catch (_) { String _$failedField; try { @@ -746,6 +772,8 @@ class CourseBuilder implements Builder { _sections?.build(); _$failedField = 'settings'; _settings?.build(); + _$failedField = 'gradingScheme'; + _gradingScheme?.build(); } catch (e) { throw new BuiltValueNestedFieldError( 'Course', _$failedField, e.toString()); diff --git a/apps/flutter_parent/lib/models/grade_cell_data.dart b/apps/flutter_parent/lib/models/grade_cell_data.dart index 5cc6740ce4..b9c0d5d11f 100644 --- a/apps/flutter_parent/lib/models/grade_cell_data.dart +++ b/apps/flutter_parent/lib/models/grade_cell_data.dart @@ -16,6 +16,7 @@ import 'package:built_value/built_value.dart'; import 'package:flutter/material.dart' hide Builder; import 'package:flutter/rendering.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; +import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/models/submission.dart'; import 'package:flutter_parent/utils/core_extensions/date_time_extensions.dart'; import 'package:flutter_parent/utils/design/parent_colors.dart'; @@ -61,16 +62,21 @@ abstract class GradeCellData implements Built 0.0) { @@ -144,7 +155,7 @@ abstract class GradeCellData implements Built b ..state = GradeCellState.graded diff --git a/apps/flutter_parent/lib/models/grading_scheme_item.dart b/apps/flutter_parent/lib/models/grading_scheme_item.dart new file mode 100644 index 0000000000..64010dda6b --- /dev/null +++ b/apps/flutter_parent/lib/models/grading_scheme_item.dart @@ -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 . + */ + +import 'package:built_value/built_value.dart'; +import 'package:built_value/json_object.dart'; +import 'package:built_value/serializer.dart'; + +part 'grading_scheme_item.g.dart'; + +abstract class GradingSchemeItem implements Built { + static Serializer get serializer => _$gradingSchemeItemSerializer; + + GradingSchemeItem._(); + + factory GradingSchemeItem([void Function(GradingSchemeItemBuilder) updates]) = _$GradingSchemeItem; + + factory GradingSchemeItem.fromJson(JsonObject json) { + if (!json.isList) return null; + List items = json.asList; + if (!(items[0] is String) || !(items[1] is num)) return null; + String grade = items[0] as String; + double value = (items[1] as num).toDouble(); + return GradingSchemeItem((b) => b + ..grade = grade + ..value = value); + } + + String get grade; + + double get value; +} diff --git a/apps/flutter_parent/lib/models/grading_scheme_item.g.dart b/apps/flutter_parent/lib/models/grading_scheme_item.g.dart new file mode 100644 index 0000000000..684f7dc435 --- /dev/null +++ b/apps/flutter_parent/lib/models/grading_scheme_item.g.dart @@ -0,0 +1,154 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'grading_scheme_item.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$gradingSchemeItemSerializer = + new _$GradingSchemeItemSerializer(); + +class _$GradingSchemeItemSerializer + implements StructuredSerializer { + @override + final Iterable types = const [GradingSchemeItem, _$GradingSchemeItem]; + @override + final String wireName = 'GradingSchemeItem'; + + @override + 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)), + ]; + + return result; + } + + @override + GradingSchemeItem deserialize( + 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; + iterator.moveNext(); + final Object value = iterator.current; + switch (key) { + case 'grade': + result.grade = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'value': + result.value = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + } + } + + return result.build(); + } +} + +class _$GradingSchemeItem extends GradingSchemeItem { + @override + final String grade; + @override + final double value; + + factory _$GradingSchemeItem( + [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'); + } + + @override + GradingSchemeItem rebuild(void Function(GradingSchemeItemBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + GradingSchemeItemBuilder toBuilder() => + new GradingSchemeItemBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is GradingSchemeItem && + grade == other.grade && + value == other.value; + } + + @override + int get hashCode { + return $jf($jc($jc(0, grade.hashCode), value.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('GradingSchemeItem') + ..add('grade', grade) + ..add('value', value)) + .toString(); + } +} + +class GradingSchemeItemBuilder + implements Builder { + _$GradingSchemeItem _$v; + + 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; + + GradingSchemeItemBuilder(); + + GradingSchemeItemBuilder get _$this { + final $v = _$v; + if ($v != null) { + _grade = $v.grade; + _value = $v.value; + _$v = null; + } + return this; + } + + @override + void replace(GradingSchemeItem other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$GradingSchemeItem; + } + + @override + 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')); + 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 diff --git a/apps/flutter_parent/lib/models/serializers.dart b/apps/flutter_parent/lib/models/serializers.dart index ee17cf2e00..1b3c08e964 100644 --- a/apps/flutter_parent/lib/models/serializers.dart +++ b/apps/flutter_parent/lib/models/serializers.dart @@ -16,6 +16,7 @@ library serializers; import 'package:built_collection/built_collection.dart'; import 'package:built_value/iso_8601_date_time_serializer.dart'; +import 'package:built_value/json_object.dart'; import 'package:built_value/serializer.dart'; import 'package:built_value/standard_json_plugin.dart'; import 'package:flutter_parent/models/account_creation_models/create_account_post_body.dart'; @@ -47,6 +48,7 @@ import 'package:flutter_parent/models/enrollment.dart'; import 'package:flutter_parent/models/grade.dart'; import 'package:flutter_parent/models/grading_period.dart'; import 'package:flutter_parent/models/grading_period_response.dart'; +import 'package:flutter_parent/models/grading_scheme_item.dart'; import 'package:flutter_parent/models/help_link.dart'; import 'package:flutter_parent/models/help_links.dart'; import 'package:flutter_parent/models/lock_info.dart'; @@ -172,6 +174,7 @@ part 'serializers.g.dart'; User, UserColors, UserNameData, + GradingSchemeItem, ]) final Serializers _serializers = _$_serializers; diff --git a/apps/flutter_parent/lib/models/serializers.g.dart b/apps/flutter_parent/lib/models/serializers.g.dart index 3847979f9c..0806afda58 100644 --- a/apps/flutter_parent/lib/models/serializers.g.dart +++ b/apps/flutter_parent/lib/models/serializers.g.dart @@ -49,6 +49,7 @@ Serializers _$_serializers = (new Serializers().toBuilder() ..add(GradeSubmissionWrapper.serializer) ..add(GradingPeriod.serializer) ..add(GradingPeriodResponse.serializer) + ..add(GradingSchemeItem.serializer) ..add(GradingType.serializer) ..add(HelpLink.serializer) ..add(HelpLinks.serializer) @@ -114,6 +115,9 @@ Serializers _$_serializers = (new Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(Section)]), () => new ListBuilder
()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(JsonObject)]), + () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(GradingPeriod)]), () => new ListBuilder()) diff --git a/apps/flutter_parent/lib/network/api/course_api.dart b/apps/flutter_parent/lib/network/api/course_api.dart index cc12d6f124..9ce9f502c7 100644 --- a/apps/flutter_parent/lib/network/api/course_api.dart +++ b/apps/flutter_parent/lib/network/api/course_api.dart @@ -38,6 +38,7 @@ class CourseApi { 'sections', 'observed_users', 'settings', + 'grading_scheme' ], 'enrollment_state': 'active', }; @@ -58,6 +59,7 @@ class CourseApi { 'course_image', 'observed_users', 'settings', + 'grading_scheme' ] }; return fetch(canvasDio(forceRefresh: forceRefresh).get('courses/${courseId}', queryParameters: params)); 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 766094f36e..72d0fa5a88 100644 --- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart +++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart @@ -185,7 +185,7 @@ class _AssignmentDetailsScreenState extends State { style: textTheme.subtitle1, key: Key("assignment_details_due_date")), ), ], - GradeCell.forSubmission(context, course?.settings?.restrictQuantitativeData ?? false, assignment, submission), + GradeCell.forSubmission(context, course, assignment, submission), ..._lockedRow(assignment), Divider(), ..._rowTile( diff --git a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart index 4a8e60b122..1f74cdfb8b 100644 --- a/apps/flutter_parent/lib/screens/assignments/grade_cell.dart +++ b/apps/flutter_parent/lib/screens/assignments/grade_cell.dart @@ -28,12 +28,12 @@ class GradeCell extends StatelessWidget { GradeCell.forSubmission( BuildContext context, - bool restrictQuantitativeData, + Course course, Assignment assignment, Submission submission, { Key key, }) : data = GradeCellData.forSubmission( - restrictQuantitativeData, + course, assignment, submission, Theme.of(context), 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 33ef1e8f94..b14a450c75 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 @@ -77,7 +77,7 @@ class CourseDetailsModel extends BaseModel { Future loadAssignments({bool forceRefresh = false}) async { if (forceRefresh) { - course = await _interactor().loadCourse(courseId); + course = await _interactor().loadCourse(courseId, forceRefresh: forceRefresh); } final groupFuture = _interactor() 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 46756cee46..005650dab6 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 @@ -145,7 +145,7 @@ class _CourseDetailsScreenState extends State with SingleTi return TabBarView( controller: _tabController, children: [ - CourseGradesScreen(model.restrictQuantitativeData), + CourseGradesScreen(), if (model.hasHomePageAsFrontPage) CourseFrontPageScreen(courseId: model.courseId), if (model.hasHomePageAsSyllabus) CourseSyllabusScreen(model.course.syllabusBody), if (model.showSummary) CourseSummaryScreen(), 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 57d692164f..21ec29988d 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 @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/assignment.dart'; import 'package:flutter_parent/models/assignment_group.dart'; +import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/models/course_grade.dart'; import 'package:flutter_parent/models/enrollment.dart'; import 'package:flutter_parent/models/grading_period.dart'; @@ -36,10 +37,6 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class CourseGradesScreen extends StatefulWidget { - final bool _restrictQuantitativeData; - - CourseGradesScreen(this._restrictQuantitativeData); - @override _CourseGradesScreenState createState() => _CourseGradesScreenState(); } @@ -116,12 +113,12 @@ class _CourseGradesScreenState extends State with AutomaticK return ListView( children: [ header, - ..._assignmentListChildren(context, snapshot.data.assignmentGroups), + ..._assignmentListChildren(context, snapshot.data.assignmentGroups, model.course), ], ); } - List _assignmentListChildren(BuildContext context, List groups) { + List _assignmentListChildren(BuildContext context, List groups, Course course) { final children = List(); for (AssignmentGroup group in groups) { @@ -153,7 +150,7 @@ class _CourseGradesScreenState extends State with AutomaticK ), children: [ ...(group.assignments.toList()..sort((a, b) => a.position.compareTo(b.position))) - .map((assignment) => _AssignmentRow(assignment: assignment, restrictQuantitativeData: widget._restrictQuantitativeData)) + .map((assignment) => _AssignmentRow(assignment: assignment, course: course)) ], ), ), @@ -281,9 +278,9 @@ class _CourseGradeHeader extends StatelessWidget { class _AssignmentRow extends StatelessWidget { final Assignment assignment; - final bool restrictQuantitativeData; + final Course course; - const _AssignmentRow({Key key, this.assignment, this.restrictQuantitativeData}) : super(key: key); + const _AssignmentRow({Key key, this.assignment, this.course}) : super(key: key); @override Widget build(BuildContext context) { @@ -378,15 +375,33 @@ class _AssignmentRow extends StatelessWidget { final localizations = L10n(context); final submission = assignment.submission(studentId); + + final restrictQuantitativeData = course?.settings?.restrictQuantitativeData ?? false; + if (submission?.excused ?? false) { - text = restrictQuantitativeData ? localizations.excused : localizations.gradeFormatScoreOutOfPointsPossible(localizations.excused, points); - semantics = restrictQuantitativeData ? localizations.excused : localizations.contentDescriptionScoreOutOfPointsPossible(localizations.excused, points); + text = restrictQuantitativeData + ? localizations.excused + : localizations.gradeFormatScoreOutOfPointsPossible(localizations.excused, points); + semantics = restrictQuantitativeData + ? localizations.excused + : localizations.contentDescriptionScoreOutOfPointsPossible(localizations.excused, points); } else if (submission?.grade != null) { - text = _formatGradeText(restrictQuantitativeData, submission.grade, points, localizations); - semantics = _formatGradeSemantics(restrictQuantitativeData, submission.grade, points, localizations); + String grade = restrictQuantitativeData && assignment.isGradingTypeQuantitative() + ? course.convertScoreToLetterGrade(submission.score, assignment.pointsPossible) + : submission.grade; + text = restrictQuantitativeData + ? grade + : localizations.gradeFormatScoreOutOfPointsPossible(grade, points); + semantics = restrictQuantitativeData + ? grade + : localizations.contentDescriptionScoreOutOfPointsPossible(grade, points); } else { - text = _formatGradeText(restrictQuantitativeData, localizations.assignmentNoScore, points, localizations); - semantics = _formatGradeSemantics(restrictQuantitativeData, '', points, localizations); // Read as "out of x points" + text = restrictQuantitativeData + ? localizations.assignmentNoScore + : localizations.gradeFormatScoreOutOfPointsPossible(localizations.assignmentNoScore, points); + semantics = restrictQuantitativeData + ? '' + : localizations.contentDescriptionScoreOutOfPointsPossible('', points); } return Text(text, @@ -395,22 +410,6 @@ class _AssignmentRow extends StatelessWidget { key: Key("assignment_${assignment.id}_grade")); } - String _formatGradeText(bool restrictQuantitativeData, String score, String pointsPossible, AppLocalizations localizations) { - if (restrictQuantitativeData) { - return !assignment.isGradingTypeQuantitative() ? score : ''; - } else { - return localizations.gradeFormatScoreOutOfPointsPossible(score, pointsPossible); - } - } - - String _formatGradeSemantics(bool restrictQuantitativeData, String score, String pointsPossible, AppLocalizations localizations) { - if (restrictQuantitativeData) { - return !assignment.isGradingTypeQuantitative() ? score : ''; - } else { - return localizations.contentDescriptionScoreOutOfPointsPossible(score, pointsPossible); - } - } - 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/inbox/create_conversation/create_conversation_screen.dart b/apps/flutter_parent/lib/screens/inbox/create_conversation/create_conversation_screen.dart index 911f5db614..1cdbf7b6e8 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 @@ -463,7 +463,7 @@ class _CreateConversationScreenState extends State wit style: Theme.of(context).textTheme.bodyText1, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( - hintText: L10n(context).messageSubjectInputHint, + labelText: L10n(context).messageSubjectInputHint, contentPadding: EdgeInsets.all(16), border: InputBorder.none, ), @@ -484,7 +484,7 @@ class _CreateConversationScreenState extends State wit maxLines: null, style: Theme.of(context).textTheme.bodyText2, decoration: InputDecoration( - hintText: L10n(context).messageBodyInputHint, + labelText: L10n(context).messageBodyInputHint, contentPadding: EdgeInsets.all(16), border: InputBorder.none, ), diff --git a/apps/flutter_parent/lib/screens/login_landing_screen.dart b/apps/flutter_parent/lib/screens/login_landing_screen.dart index 7c66ea9cf9..af81ff724f 100644 --- a/apps/flutter_parent/lib/screens/login_landing_screen.dart +++ b/apps/flutter_parent/lib/screens/login_landing_screen.dart @@ -18,19 +18,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/login.dart'; import 'package:flutter_parent/models/school_domain.dart'; -import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/qr_login/qr_login_util.dart'; import 'package:flutter_parent/screens/web_login/web_login_screen.dart'; import 'package:flutter_parent/utils/common_widgets/avatar.dart'; -import 'package:flutter_parent/utils/common_widgets/error_report/error_report_dialog.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/common_widgets/two_finger_double_tap_gesture_detector.dart'; import 'package:flutter_parent/utils/common_widgets/user_name.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; -import 'package:flutter_parent/utils/design/canvas_icons.dart'; import 'package:flutter_parent/utils/design/canvas_icons_solid.dart'; import 'package:flutter_parent/utils/design/parent_colors.dart'; import 'package:flutter_parent/utils/design/parent_theme.dart'; @@ -93,13 +88,14 @@ 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'; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Spacer(), SvgPicture.asset( - 'assets/svg/canvas-parent-login-logo.svg', + assetString, semanticsLabel: L10n(context).canvasLogoLabel, ), Spacer(), diff --git a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart index 74dffb7968..652a26fc44 100644 --- a/apps/flutter_parent/lib/screens/settings/settings_interactor.dart +++ b/apps/flutter_parent/lib/screens/settings/settings_interactor.dart @@ -96,6 +96,7 @@ class SettingsInteractor { SvgPicture.asset( 'assets/svg/ic_instructure_logo.svg', alignment: Alignment.bottomCenter, + semanticsLabel: L10n(context).aboutLogoSemanticsLabel, ) ], ), diff --git a/apps/flutter_parent/lib/utils/alert_helper.dart b/apps/flutter_parent/lib/utils/alert_helper.dart index c2c378792d..cdd947e54f 100644 --- a/apps/flutter_parent/lib/utils/alert_helper.dart +++ b/apps/flutter_parent/lib/utils/alert_helper.dart @@ -26,7 +26,7 @@ class AlertsHelper { filteredList.add(element); } else { Course course = await locator().getCourse(courseId, forceRefresh: false); - if (!course.settings.restrictQuantitativeData) { + if (!(course.settings?.restrictQuantitativeData ?? false)) { filteredList.add(element); } } diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index 021b319332..c87f6a3c72 100644 --- a/apps/flutter_parent/pubspec.yaml +++ b/apps/flutter_parent/pubspec.yaml @@ -25,7 +25,7 @@ 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.7.0+45 +version: 3.8.0+46 module: androidX: true diff --git a/apps/flutter_parent/test/models/course_test.dart b/apps/flutter_parent/test/models/course_test.dart index c9af1a4519..ef7b2cdee4 100644 --- a/apps/flutter_parent/test/models/course_test.dart +++ b/apps/flutter_parent/test/models/course_test.dart @@ -12,11 +12,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/models/course_grade.dart'; import 'package:flutter_parent/models/enrollment.dart'; -import 'package:flutter_parent/models/section.dart'; -import 'package:flutter_parent/models/term.dart'; +import 'package:flutter_parent/models/grading_scheme_item.dart'; import 'package:test/test.dart'; void main() { @@ -111,4 +111,44 @@ void main() { expect(isValid, isFalse); }); }); + + group('gradingScheme', () { + final gradingSchemeBuilder = ListBuilder() + ..add(JsonObject(["A", 0.9])) + ..add(JsonObject(["F", 0.0])); + + final course = _course.rebuild((b) => b..gradingScheme = gradingSchemeBuilder); + + test('returns "F" for 70 percent', () { + expect(course.convertScoreToLetterGrade(70, 100), 'F'); + }); + + test('returns "A" for 90 percent', () { + expect(course.convertScoreToLetterGrade(90, 100), 'A'); + }); + + test('returns empty if max score is 0', () { + expect(course.convertScoreToLetterGrade(10, 0), ''); + }); + + test('returns empty if grading scheme is null or empty', () { + final course = _course.rebuild((b) => b..gradingScheme = null); + expect(course.convertScoreToLetterGrade(10, 0), ''); + }); + + test('grading scheme mapping filters out incorrect items', () { + gradingSchemeBuilder.add(JsonObject("")); + gradingSchemeBuilder.add(JsonObject([1, 0.9])); + gradingSchemeBuilder.add(JsonObject(["C", "3"])); + final course = _course.rebuild((b) => b..gradingScheme = gradingSchemeBuilder); + expect(course.gradingSchemeItems, [ + GradingSchemeItem((b) => b + ..grade = "A" + ..value = 0.9), + GradingSchemeItem((b) => b + ..grade = "F" + ..value = 0.0), + ]); + }); + }); } 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 95f11361fb..32f6503b76 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 @@ -12,9 +12,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/assignment.dart'; +import 'package:flutter_parent/models/course.dart'; import 'package:flutter_parent/models/grade_cell_data.dart'; import 'package:flutter_parent/models/submission.dart'; import 'package:flutter_parent/utils/core_extensions/date_time_extensions.dart'; @@ -26,6 +29,7 @@ void main() { Assignment baseAssignment; Submission baseSubmission; GradeCellData baseGradedState; + Course baseCourse; Color accentColor = Colors.pinkAccent; @@ -55,11 +59,20 @@ void main() { ..state = GradeCellState.graded ..accentColor = accentColor ..outOf = 'Out of 100 points'); + + final gradingSchemeBuilder = ListBuilder() + ..add(JsonObject(["A", 0.9])) + ..add(JsonObject(["F", 0.0])); + + baseCourse = Course((b) => b + ..id = '123' + ..settings.restrictQuantitativeData = false + ..gradingScheme = gradingSchemeBuilder); }); test('Returns empty for null submission', () { var expected = GradeCellData(); - var actual = GradeCellData.forSubmission(false, baseAssignment, null, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, null, theme, l10n); expect(actual, expected); }); @@ -69,7 +82,7 @@ void main() { ..graphPercent = 0.85 ..score = '85' ..showPointsLabel = true); - var actual = GradeCellData.forSubmission(false, baseAssignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); }); @@ -81,7 +94,7 @@ void main() { ..grade = null ..score = 0.0); var expected = GradeCellData(); - var actual = GradeCellData.forSubmission(false, baseAssignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); }); @@ -97,14 +110,14 @@ void main() { l10n.submissionStatusSuccessSubtitle, dateFormat: DateFormat.MMMMd(), )); - var actual = GradeCellData.forSubmission(false, baseAssignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); }); test('Returns Empty state when not submitted and ungraded', () { var submission = Submission((b) => b..assignmentId = '1'); var expected = GradeCellData(); - var actual = GradeCellData.forSubmission(false, baseAssignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); }); @@ -114,7 +127,7 @@ void main() { ..graphPercent = 1.0 ..showCompleteIcon = true ..grade = 'Excused'); - var actual = GradeCellData.forSubmission(false, baseAssignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); }); @@ -127,7 +140,7 @@ void main() { ..showPointsLabel = true ..grade = '85%' ..gradeContentDescription = '85%'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -138,7 +151,7 @@ void main() { ..graphPercent = 1.0 ..showCompleteIcon = true ..grade = 'Complete'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -152,7 +165,7 @@ void main() { ..graphPercent = 1.0 ..showIncompleteIcon = true ..grade = 'Incomplete'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -167,7 +180,7 @@ void main() { l10n.submissionStatusSuccessSubtitle, dateFormat: DateFormat.MMMMd(), )); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -176,7 +189,7 @@ void main() { ..graphPercent = 0.85 ..score = '85' ..showPointsLabel = true); - var actual = GradeCellData.forSubmission(false, baseAssignment, baseSubmission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, baseSubmission, theme, l10n); expect(actual, expected); }); @@ -189,7 +202,7 @@ void main() { ..showPointsLabel = true ..grade = 'B+' ..gradeContentDescription = 'B+'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -206,7 +219,7 @@ void main() { ..showPointsLabel = true ..grade = 'A-' ..gradeContentDescription = 'A. minus'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -219,7 +232,7 @@ void main() { ..showPointsLabel = true ..grade = '3.8 GPA' ..gradeContentDescription = '3.8 GPA'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -227,7 +240,7 @@ void main() { var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.notGraded); var submission = Submission((b) => b..assignmentId = '1'); var expected = GradeCellData(); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -242,7 +255,7 @@ void main() { ..showPointsLabel = true ..latePenalty = 'Late penalty (-6)' ..finalGrade = 'Final Grade: 79'); - var actual = GradeCellData.forSubmission(false, baseAssignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, baseAssignment, submission, theme, l10n); expect(actual, expected); }); @@ -255,7 +268,7 @@ void main() { ..showPointsLabel = true ..grade = 'B-' ..gradeContentDescription = 'B. minus'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); @@ -272,33 +285,40 @@ void main() { ..showPointsLabel = true ..grade = 'B' ..gradeContentDescription = 'B'); - var actual = GradeCellData.forSubmission(false, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(baseCourse, assignment, submission, theme, l10n); expect(actual, expected); }); - test('Returns Empty state when quantitative data is restricted and grading type is points and not excused', () { + test('Returns Empty state when quantitative data is restricted, grading type is points, grading scheme is null and not excused', () { + var course = baseCourse.rebuild((b) => b + ..settings.restrictQuantitativeData = true + ..gradingScheme = null); var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.points); var submission = Submission((b) => b ..assignmentId = '1' ..score = 10.0 ..grade = 'A'); var expected = GradeCellData(); - var actual = GradeCellData.forSubmission(true, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(course, assignment, submission, theme, l10n); expect(actual, expected); }); - test('Returns Empty state when quantitative data is restricted and grading type is percent and not excused', () { + test('Returns Empty state when quantitative data is restricted, grading type is percent, grading scheme is null and not excused', () { + var course = baseCourse.rebuild((b) => b + ..settings.restrictQuantitativeData = true + ..gradingScheme = null); var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.percent); var submission = Submission((b) => b ..assignmentId = '1' ..score = 10.0 ..grade = 'A'); var expected = GradeCellData(); - var actual = GradeCellData.forSubmission(true, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(course, assignment, submission, theme, l10n); expect(actual, expected); }); test('Returns correct state when quantitative data is restricted and grading type is percent and excused', () { + var course = baseCourse.rebuild((b) => b..settings.restrictQuantitativeData = true); var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.percent); var submission = Submission((b) => b ..assignmentId = '1' @@ -310,11 +330,12 @@ void main() { ..grade = l10n.excused ..outOf = '' ..showCompleteIcon = true); - var actual = GradeCellData.forSubmission(true, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(course, assignment, submission, theme, l10n); expect(actual, expected); }); test('Returns correct state when quantitative data is restricted and graded', () { + var course = baseCourse.rebuild((b) => b..settings.restrictQuantitativeData = true); var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.letterGrade); var submission = Submission((b) => b ..assignmentId = '1' @@ -326,7 +347,41 @@ void main() { ..score = submission.grade ..gradeContentDescription = submission.grade ..outOf = ''); - var actual = GradeCellData.forSubmission(true, assignment, submission, theme, l10n); + var actual = GradeCellData.forSubmission(course, assignment, submission, theme, l10n); + expect(actual, expected); + }); + + test('Returns correct state when quantitative data is restricted, grading type is percent and grading scheme is given', () { + var course = baseCourse.rebuild((b) => b..settings.restrictQuantitativeData = true); + var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.percent); + var submission = Submission((b) => b + ..assignmentId = '1' + ..score = 10.0 + ..grade = 'A'); + var expected = baseGradedState.rebuild((b) => b + ..state = GradeCellState.gradedRestrictQuantitativeData + ..graphPercent = 1.0 + ..score = 'F' + ..outOf = '' + ..gradeContentDescription = 'F'); + var actual = GradeCellData.forSubmission(course, assignment, submission, theme, l10n); + expect(actual, expected); + }); + + test('Returns correct state when quantitative data is restricted, grading type is point and grading scheme is given', () { + var course = baseCourse.rebuild((b) => b..settings.restrictQuantitativeData = true); + var assignment = baseAssignment.rebuild((b) => b..gradingType = GradingType.points); + var submission = Submission((b) => b + ..assignmentId = '1' + ..score = 90.0 + ..grade = 'A'); + var expected = baseGradedState.rebuild((b) => b + ..state = GradeCellState.gradedRestrictQuantitativeData + ..graphPercent = 1.0 + ..score = 'A' + ..outOf = '' + ..gradeContentDescription = 'A'); + var actual = GradeCellData.forSubmission(course, assignment, submission, theme, l10n); expect(actual, expected); }); } 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 9a8e14d41b..bf2c9aff64 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 @@ -15,6 +15,7 @@ import 'dart:convert'; import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; import 'package:flutter/material.dart'; import 'package:flutter_parent/l10n/app_localizations.dart'; import 'package:flutter_parent/models/assignment.dart'; @@ -320,6 +321,64 @@ void main() { expect(find.text(AppLocalizations().assignmentNotSubmittedLabel), findsOneWidget); }); + testWidgetsWithAccessibilityChecks('Shows grade with possible points if not restricted', (tester) async { + final grade = 'FFF'; + final group = _mockAssignmentGroup(assignments: [_mockAssignment(id: '1', pointsPossible: 2.2, submission: _mockSubmission(grade: grade))]); + final enrollment = Enrollment((b) => b..enrollmentState = 'active'); + + 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())); + when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); + model.course = _mockCourse(); + + await tester.pumpWidget(_testableWidget(model)); + await tester.pump(); // Build the widget + await tester.pump(); // Let the future finish + + expect(find.text('$grade / 2.2'), findsOneWidget); + }); + + testWidgetsWithAccessibilityChecks('Shows grade without possible points if restricted', (tester) async { + final grade = 'FFF'; + final group = _mockAssignmentGroup(assignments: [_mockAssignment(id: '1', pointsPossible: 2.2, submission: _mockSubmission(grade: grade))]); + final enrollment = Enrollment((b) => b..enrollmentState = 'active'); + + 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())); + when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); + model.course = _mockCourse().rebuild((b) => b..settings.restrictQuantitativeData = true); + + await tester.pumpWidget(_testableWidget(model)); + await tester.pump(); // Build the widget + await tester.pump(); // Let the future finish + + expect(find.text('$grade'), findsOneWidget); + }); + + testWidgetsWithAccessibilityChecks('Shows grade by score and grading scheme if restricted', (tester) async { + final group = _mockAssignmentGroup(assignments: [ + _mockAssignment(id: '1', pointsPossible: 10, gradingType: GradingType.points, submission: _mockSubmission(grade: '', score: 1.0)) + ]); + final enrollment = Enrollment((b) => b..enrollmentState = 'active'); + + 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())); + when(interactor.loadEnrollmentsForGradingPeriod(_courseId, _studentId, null)).thenAnswer((_) async => [enrollment]); + model.course = _mockCourse().rebuild((b) => b..settings.restrictQuantitativeData = true); + + await tester.pumpWidget(_testableWidget(model)); + await tester.pump(); // Build the widget + await tester.pump(); // Let the future finish + + expect(find.text('F'), findsOneWidget); + }); + group('CourseGradeHeader', () { testWidgetsWithAccessibilityChecks('from current score, max 2 digits', (tester) async { final groups = [ @@ -700,6 +759,10 @@ void main() { }); } +final _gradingSchemeBuilder = ListBuilder() + ..add(JsonObject(["A", 0.9])) + ..add(JsonObject(["F", 0.0])); + Course _mockCourse() { return Course((b) => b ..id = _courseId @@ -709,7 +772,8 @@ Course _mockCourse() { ..userId = _studentId ..courseId = _courseId ..enrollmentState = 'active') - ]).toBuilder()); + ]).toBuilder() + ..gradingScheme = _gradingSchemeBuilder); } GradeBuilder _mockGrade({double currentScore, double finalScore, String currentGrade, String finalGrade}) { @@ -739,6 +803,7 @@ Assignment _mockAssignment({ Submission submission, DateTime dueAt, double pointsPossible = 0, + GradingType gradingType }) { return Assignment((b) => b ..id = id @@ -747,26 +812,27 @@ Assignment _mockAssignment({ ..assignmentGroupId = groupId ..position = int.parse(id) ..dueAt = dueAt - ..submissionWrapper = SubmissionWrapper( - (b) => b..submissionList = BuiltList.from(submission != null ? [submission] : []).toBuilder()) - .toBuilder() + ..submissionWrapper = + SubmissionWrapper((b) => b..submissionList = BuiltList.from(submission != null ? [submission] : []).toBuilder()).toBuilder() ..pointsPossible = pointsPossible - ..published = true); + ..published = true + ..gradingType = gradingType); } -Submission _mockSubmission({String assignmentId = '', String grade, bool isLate, DateTime submittedAt}) { +Submission _mockSubmission({String assignmentId = '', String grade, bool isLate, DateTime submittedAt, double score}) { return Submission((b) => b ..userId = _studentId ..assignmentId = assignmentId ..grade = grade ..submittedAt = submittedAt - ..isLate = isLate ?? false); + ..isLate = isLate ?? false + ..score = score ?? 0); } Widget _testableWidget(CourseDetailsModel model, {PlatformConfig platformConfig = const PlatformConfig()}) { return TestApp( Scaffold( - body: ChangeNotifierProvider.value(value: model, child: CourseGradesScreen(false)), + body: ChangeNotifierProvider.value(value: model, child: CourseGradesScreen()), ), platformConfig: platformConfig, ); 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/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/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt index 524f3a9c29..e4cdad1e9c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt @@ -48,7 +48,7 @@ class SyllabusE2ETest: StudentTest() { fun testSyllabusE2E() { Log.d(PREPARATION_TAG, "Seeding data.") - val data = seedData(students = 1, teachers = 1, courses = 1) + val data = seedData(students = 1, teachers = 1, courses = 1, syllabusBody = "this is the syllabus body") val student = data.studentsList[0] val teacher = data.teachersList[0] val course = data.coursesList[0] @@ -60,9 +60,10 @@ class SyllabusE2ETest: StudentTest() { Log.d(STEP_TAG,"Select ${course.name} course.") dashboardPage.selectCourse(course) - Log.d(STEP_TAG,"Navigate to Syllabus Page. Assert that Empty View is displayed, because there is no syllabus yet.") + Log.d(STEP_TAG,"Navigate to Syllabus Page. Assert that the syllabus body string is displayed, and there are no tabs yet.") courseBrowserPage.selectSyllabus() - syllabusPage.assertEmptyView() + syllabusPage.assertNoTabs() + syllabusPage.assertSyllabusBody("this is the syllabus body") Log.d(PREPARATION_TAG,"Seed an assignment for ${course.name} course.") val assignment = createAssignment(course, teacher) @@ -70,10 +71,9 @@ class SyllabusE2ETest: StudentTest() { Log.d(PREPARATION_TAG,"Seed a quiz for ${course.name} course.") val quiz = createQuiz(course, teacher) - // TODO: Seed a generic calendar event - - Log.d(STEP_TAG,"Refresh the page. Assert that all of the items, so ${assignment.name} assignment and ${quiz.title} quiz are displayed.") + Log.d(STEP_TAG,"Refresh the page. Navigate to 'Summary' tab. Assert that all of the items, so ${assignment.name} assignment and ${quiz.title} quiz are displayed.") syllabusPage.refresh() + syllabusPage.selectSummaryTab() syllabusPage.assertItemDisplayed(assignment.name) syllabusPage.assertItemDisplayed(quiz.title) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt index 8be34b0815..b78a0e9db0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentDetailsInteractionTest.kt @@ -301,11 +301,11 @@ class AssignmentDetailsInteractionTest : StudentTest() { @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false) fun testPointsAssignmentWithQuantitativeRestriction() { setUpData(restrictQuantitativeData = true) - val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "90", 90.0, 100) + val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "65", 65.0, 100) goToAssignmentList() assignmentListPage.clickAssignment(assignment) - assignmentDetailsPage.assertGradeNotDisplayed() + assignmentDetailsPage.assertGradeDisplayed("D") assignmentDetailsPage.assertOutOfTextNotDisplayed() assignmentDetailsPage.assertScoreNotDisplayed() } @@ -327,11 +327,11 @@ class AssignmentDetailsInteractionTest : StudentTest() { @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false) fun testPercentageAssignmentWithQuantitativeRestriction() { setUpData(restrictQuantitativeData = true) - val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "90%", 90.0, 100) + val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "70%", 70.0, 100) goToAssignmentList() assignmentListPage.clickAssignment(assignment) - assignmentDetailsPage.assertGradeNotDisplayed() + assignmentDetailsPage.assertGradeDisplayed("C") assignmentDetailsPage.assertOutOfTextNotDisplayed() assignmentDetailsPage.assertScoreNotDisplayed() } @@ -358,8 +358,17 @@ class AssignmentDetailsInteractionTest : StudentTest() { val course = data.courses.values.first() + val gradingScheme = listOf( + listOf("A", 0.9), + listOf("B", 0.8), + listOf("C", 0.7), + listOf("D", 0.6), + listOf("F", 0.0) + ) + val newCourse = course - .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData)) + .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData), + gradingSchemeRaw = gradingScheme) data.courses[course.id] = newCourse data.addAssignmentsToGroups(newCourse) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt index 8564fc53ed..b7c95d29ae 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AssignmentListInteractionTest.kt @@ -165,7 +165,7 @@ class AssignmentListInteractionTest : StudentTest() { val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.POINTS, "90", 90.0, 100) goToAssignmentsPage() - assignmentListPage.assertAssignmentDisplayedWithoutGrade(assignment.name!!) + assignmentListPage.assertAssignmentDisplayedWithGrade(assignment.name!!, "A") } @Test @@ -182,10 +182,10 @@ class AssignmentListInteractionTest : StudentTest() { @TestMetaData(Priority.IMPORTANT, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION, false) fun testPercentageAssignmentWithQuantitativeRestriction() { setUpData(restrictQuantitativeData = true) - val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "90%", 90.0, 100) + val assignment = addAssignment(MockCanvas.data, Assignment.GradingType.PERCENT, "80%", 80.0, 100) goToAssignmentsPage() - assignmentListPage.assertAssignmentDisplayedWithoutGrade(assignment.name!!) + assignmentListPage.assertAssignmentDisplayedWithGrade(assignment.name!!, "B") } @Test @@ -208,8 +208,17 @@ class AssignmentListInteractionTest : StudentTest() { val course = data.courses.values.first() + val gradingScheme = listOf( + listOf("A", 0.9), + listOf("B", 0.8), + listOf("C", 0.7), + listOf("D", 0.6), + listOf("F", 0.0) + ) + val newCourse = course - .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData)) + .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData), + gradingSchemeRaw = gradingScheme) data.courses[course.id] = newCourse val assignmentList = mutableListOf() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt index 8471280aa4..ec8a032701 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/CourseGradesInteractionTest.kt @@ -70,11 +70,11 @@ class CourseGradesInteractionTest : StudentTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.GRADES, TestCategory.INTERACTION, false) - fun testNAIsDisplayedWithOnlyScoreWhenRestrictedAndThereIsNoGrade() { + fun testConvertedGradeIsDisplayedWithOnlyScoreWhenRestrictedAndThereIsNoGrade() { val data = setUpData(courseCount = 1, favoriteCourseCount = 1) setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true) goToGrades(data) - courseGradesPage.assertTotalGrade(ViewMatchers.withText(courseGradesPage.getStringFromResource(R.string.noGradeText))) + courseGradesPage.assertTotalGrade(ViewMatchers.withText("A")) } @Test @@ -182,7 +182,7 @@ class CourseGradesInteractionTest : StudentTest() { goToGrades(data) - courseGradesPage.assertAssignmentDisplayedWithoutGrade(assignment.name!!) + courseGradesPage.assertAssignmentDisplayed(assignment.name!!, "A") } @Test @@ -202,11 +202,11 @@ class CourseGradesInteractionTest : StudentTest() { fun testPercentageAssignmentWithQuantitativeRestriction() { val data = setUpData(courseCount = 1, favoriteCourseCount = 1) setUpCustomGrade(score = 100.0, data = data, restrictQuantitativeData = true) - val assignment = addAssignment(data, Assignment.GradingType.PERCENT, "90%", 90.0, 100) + val assignment = addAssignment(data, Assignment.GradingType.PERCENT, "80%", 80.0, 100) goToGrades(data) - courseGradesPage.assertAssignmentDisplayedWithoutGrade(assignment.name!!) + courseGradesPage.assertAssignmentDisplayed(assignment.name!!, "B") } @Test @@ -281,9 +281,18 @@ class CourseGradesInteractionTest : StudentTest() { computedCurrentScore = score ) + val gradingScheme = listOf( + listOf("A", 0.9), + listOf("B", 0.8), + listOf("C", 0.7), + listOf("D", 0.6), + listOf("F", 0.0) + ) + val newCourse = course .copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData), - enrollments = mutableListOf(enrollment)) + enrollments = mutableListOf(enrollment), + gradingSchemeRaw = gradingScheme) data.courses[course.id] = newCourse } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt index 79c3f4a137..aa1c84ccdf 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt @@ -25,6 +25,7 @@ import com.instructure.canvas.espresso.mockCanvas.addFolderToCourse import com.instructure.canvas.espresso.mockCanvas.addGroupToCourse import com.instructure.canvas.espresso.mockCanvas.addPageToCourse import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.refresh import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group @@ -72,6 +73,38 @@ class GroupLinksInteractionTest : StudentTest() { dashboardPage.assertDisplaysGroup(group, course) } + // Test not favorite group on dashboard + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DASHBOARD) + fun testGroupLink_dashboard_favoriteLogics() { + val data = setUpGroupAndSignIn() + val user = data.users.values.first() + val nonFavoriteGroup = data.addGroupToCourse( + course = course, + members = listOf(user), + isFavorite = false + ) + refresh() //Need to refresh because when we navigated to Dashboard page the nonFavoriteGroup was not existed yet. (However it won't be displayed because it's not favorite) + dashboardPage.assertGroupNotDisplayed(nonFavoriteGroup) + dashboardPage.assertDisplaysGroup(group, course) + } + + // Test that if no groups has selected as favorite then we display all groups + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_DASHBOARD) + fun testGroupLink_dashboard_not_selected_displays_all() { + val data = setUpGroupAndSignIn(isFavorite = false) + val user = data.users.values.first() + val group2 = data.addGroupToCourse( + course = course, + members = listOf(user), + isFavorite = false + ) + refresh() //Need to refresh because when we navigated to Dashboard page the group2 was not existed yet. + dashboardPage.assertDisplaysGroup(group, course) + dashboardPage.assertDisplaysGroup(group2, course) + } + // Link to file preview opens file - eg: "/groups/:id/files/folder/:id?preview=:id" @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.GROUPS, TestCategory.INTERACTION, false, SecondaryFeatureCategory.GROUPS_FILES) @@ -196,7 +229,7 @@ class GroupLinksInteractionTest : StudentTest() { // Mock a single student and course, mock a group and a number of items associated with the group, // sign in, then navigate to the dashboard. - private fun setUpGroupAndSignIn(): MockCanvas { + private fun setUpGroupAndSignIn(isFavorite: Boolean = true): MockCanvas { // Basic info val data = MockCanvas.init( @@ -211,7 +244,7 @@ class GroupLinksInteractionTest : StudentTest() { group = data.addGroupToCourse( course = course, members = listOf(user), - isFavorite = true + isFavorite = isFavorite ) // Add a discussion diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index c35844fb1b..20c99a602c 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -18,12 +18,36 @@ package com.instructure.student.ui.interaction import android.text.Html 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.addItemToModule +import com.instructure.canvas.espresso.mockCanvas.addLTITool +import com.instructure.canvas.espresso.mockCanvas.addModuleToCourse +import com.instructure.canvas.espresso.mockCanvas.addPageToCourse +import com.instructure.canvas.espresso.mockCanvas.addQuestionToQuiz +import com.instructure.canvas.espresso.mockCanvas.addQuizToCourse +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.LockInfo +import com.instructure.canvasapi2.models.LockedModule +import com.instructure.canvasapi2.models.ModuleContentDetails +import com.instructure.canvasapi2.models.ModuleObject +import com.instructure.canvasapi2.models.Page +import com.instructure.canvasapi2.models.Quiz +import com.instructure.canvasapi2.models.QuizAnswer +import com.instructure.canvasapi2.models.Tab import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 -import com.instructure.panda_annotations.* +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.SecondaryFeatureCategory +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData import com.instructure.student.R import com.instructure.student.ui.pages.WebViewTextCheck import com.instructure.student.ui.utils.StudentTest @@ -43,7 +67,6 @@ class ModuleInteractionTest : StudentTest() { private var page: Page? = null private val fileName = "ModuleFile.html" private var fileCheck: WebViewTextCheck? = null - private val externalUrl = "https://www.google.com" private var quiz: Quiz? = null // Tapping an Assignment module item should navigate to that item's detail page @@ -55,7 +78,19 @@ class ModuleInteractionTest : StudentTest() { val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + // Create an assignment and add it as a module item + assignment = data.addAssignment( + courseId = course1.id, + submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = assignment!! + ) + // Verify that we can launch into the assignment from an assignment module item + modulesPage.refresh() modulesPage.assertModuleDisplayed(module) modulesPage.assertModuleItemDisplayed(module, assignment!!.name!!) modulesPage.clickModuleItem(module, assignment!!.name!!) @@ -71,8 +106,23 @@ class ModuleInteractionTest : StudentTest() { val data = getToCourseModules(studentCount = 1, courseCount = 1) val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + val user = data.users.values.first() + + // Create a discussion and add it as a module item + topicHeader = data.addDiscussionTopicToCourse( + course = course1, + user = user, + topicTitle = "Discussion in module", + topicDescription = "In. A. Module." + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = topicHeader!! + ) // Verify that we can launch into a discussion from a discussion module item + modulesPage.refresh() modulesPage.assertModuleDisplayed(module) modulesPage.assertModuleItemDisplayed(module, topicHeader!!.title!!) modulesPage.clickModuleItem(module, topicHeader!!.title!!) @@ -87,6 +137,14 @@ class ModuleInteractionTest : StudentTest() { val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + val ltiTool = data.addLTITool("Google Drive", "http://google.com", course1, 1234L) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = ltiTool!! + ) + + modulesPage.refresh() modulesPage.clickModuleItem(module, "Google Drive") canvasWebViewPage.assertTitle("Google Drive") } @@ -96,11 +154,20 @@ class ModuleInteractionTest : StudentTest() { @TestMetaData(Priority.MANDATORY, FeatureCategory.MODULES, TestCategory.INTERACTION, false) fun testModules_launchesIntoExternalURL() { // Basic mock setup + val externalUrl = "https://www.google.com" val data = getToCourseModules(studentCount = 1, courseCount = 1) val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + // Create an external URL and add it as a module item + data.addItemToModule( + course = course1, + moduleId = module.id, + item = externalUrl + ) + // click the external url module item + modulesPage.refresh() modulesPage.clickModuleItem(module, externalUrl) // Not much we can test here, as it is an external URL, but testModules_navigateToNextAndPreviousModuleItems // will test that the module name and module item name are displayed correctly. @@ -116,7 +183,26 @@ class ModuleInteractionTest : StudentTest() { val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + // Create a file and add it as a module item + val fileContent = "

A Heading

" + fileCheck = WebViewTextCheck(Locator.ID, "heading1", "A Heading") + + val fileId = data.addFileToCourse( + courseId = course1.id, + displayName = fileName, + fileContent = fileContent, + contentType = "text/html" + ) + val rootFolderId = data.courseRootFolders[course1.id]!!.id + val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId } + data.addItemToModule( + course = course1, + moduleId = module.id, + item = fileFolder!! + ) + // Click the file module and verify that the file appears + modulesPage.refresh() modulesPage.clickModuleItem(module, fileName, R.id.openButton) canvasWebViewPage.waitForWebView() canvasWebViewPage.runTextChecks(fileCheck!!) @@ -131,7 +217,22 @@ class ModuleInteractionTest : StudentTest() { val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + // Create a page and add it as a module item + page = data.addPageToCourse( + courseId = course1.id, + pageId = data.newItemId(), + published = true, + title = "Page In Course", + url = URLEncoder.encode("Page In Course", "UTF-8") + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = page!! + ) + // Verify that we can launch into a page from a page module item + modulesPage.refresh() modulesPage.assertModuleDisplayed(module) modulesPage.assertModuleItemDisplayed(module, page!!.title!!) modulesPage.clickModuleItem(module, page!!.title!!) @@ -157,7 +258,48 @@ class ModuleInteractionTest : StudentTest() { val course1 = data.courses.values.first() val module = data.courseModules[course1.id]!!.first() + // Create a quiz and add it as a module item + quiz = data.addQuizToCourse( + course = course1 + ) + + data.addQuestionToQuiz( + course = course1, + quizId = quiz!!.id, + questionName = "Math 1", + questionText = "What is 2 + 5?", + questionType = "multiple_choice_question", + answers = arrayOf( + QuizAnswer(answerText = "7"), + QuizAnswer(answerText = "25"), + QuizAnswer(answerText = "-7") + ) + ) + + data.addQuestionToQuiz( + course = course1, + quizId = quiz!!.id, + questionName = "Math 2", + questionText = "Pi is greater than the square root of 2", + questionType = "true_false_question" + ) + + data.addQuestionToQuiz( + course = course1, + quizId = quiz!!.id, + questionName = "Math 3", + questionText = "Write an essay on why math is so awesome", + questionType = "essay_question" + ) + + data.addItemToModule( + course = course1, + moduleId = module.id, + item = quiz!! + ) + // Verify that we can launch into a quiz from a quiz module item + modulesPage.refresh() modulesPage.assertModuleDisplayed(module) modulesPage.assertModuleItemDisplayed(module, quiz!!.title!!) /* TODO: Check that the quiz is displayed if/when we can do so via WebView @@ -177,7 +319,20 @@ class ModuleInteractionTest : StudentTest() { // Basic mock setup val data = getToCourseModules(studentCount = 1, courseCount = 1) val course1 = data.courses.values.first() - val module = data.courseModules[course1.id]!!.first() + var module = data.courseModules[course1.id]!!.first() + + // Create an assignment and add it as a module item + assignment = data.addAssignment( + courseId = course1.id, + submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = assignment!! + ) + + module = data.courseModules[course1.id]!!.first() val firstModuleItem = module.items[0] // Verify that expanding a module shows the module items and collapsing a module @@ -185,6 +340,7 @@ class ModuleInteractionTest : StudentTest() { // We're going on the assumption that the lone module is initially expanded. Although // the initial assertModuleItemDisplayed() would expand the module if it was not expanded // already. + modulesPage.refresh() modulesPage.assertModuleDisplayed(module) modulesPage.assertModuleItemDisplayed(module, firstModuleItem.title!!) modulesPage.clickModule(module) @@ -221,11 +377,68 @@ class ModuleInteractionTest : StudentTest() { fun testModules_navigateToNextAndPreviousModuleItems() { // Basic mock setup val data = getToCourseModules(studentCount = 1, courseCount = 1) + val externalUrl = "https://www.google.com" val course1 = data.courses.values.first() - val module = data.courseModules[course1.id]!!.first() + var module = data.courseModules[course1.id]!!.first() + val user = data.users.values.first() + + // Create an assignment and add it as a module item + assignment = data.addAssignment( + courseId = course1.id, + submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = assignment!! + ) + + // Create a discussion and add it as a module item + topicHeader = data.addDiscussionTopicToCourse( + course = course1, + user = user, + topicTitle = "Discussion in module", + topicDescription = "In. A. Module." + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = topicHeader!! + ) + + val ltiTool = data.addLTITool("Google Drive", "http://google.com", course1, 1234L) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = ltiTool!! + ) + + // Create a page and add it as a module item + page = data.addPageToCourse( + courseId = course1.id, + pageId = data.newItemId(), + published = true, + title = "Page In Course", + url = URLEncoder.encode("Page In Course", "UTF-8") + ) + data.addItemToModule( + course = course1, + moduleId = module.id, + item = page!! + ) + + // Create an external URL and add it as a module item + data.addItemToModule( + course = course1, + moduleId = module.id, + item = externalUrl + ) + + module = data.courseModules[course1.id]!!.first() // Iterate through the module items, starting at the first val moduleItemList = module.items + modulesPage.refresh() modulesPage.clickModuleItem(module, moduleItemList[0].title!!) var moduleIndex = 0; // we start here @@ -413,126 +626,15 @@ class ModuleInteractionTest : StudentTest() { // Add a course tab val course1 = data.courses.values.first() - val user1 = data.users.values.first() val modulesTab = Tab(position = 2, label = "Modules", visibility = "public", tabId = Tab.MODULES_ID) data.courseTabs[course1.id]!! += modulesTab // Create a module - val module = data.addModuleToCourse( + data.addModuleToCourse( course = course1, moduleName = "Big Module" ) - // Create a discussion and add it as a module item - topicHeader = data.addDiscussionTopicToCourse( - course = course1, - user = user1, - topicTitle = "Discussion in module", - topicDescription = "In. A. Module." - ) - data.addItemToModule( - course = course1, - moduleId = module.id, - item = topicHeader!! - ) - - // Create an assignment and add it as a module item - assignment = data.addAssignment( - courseId = course1.id, - submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY - ) - data.addItemToModule( - course = course1, - moduleId = module.id, - item = assignment!! - ) - - // Create a page and add it as a module item - page = data.addPageToCourse( - courseId = course1.id, - pageId = data.newItemId(), - published = true, - title = "Page In Course", - url = URLEncoder.encode("Page In Course", "UTF-8") - ) - data.addItemToModule( - course = course1, - moduleId = module.id, - item = page!! - ) - - // Create a file and add it as a module item - val fileContent = "

A Heading

" - fileCheck = WebViewTextCheck(Locator.ID, "heading1", "A Heading") - - val fileId = data.addFileToCourse( - courseId = course1.id, - displayName = fileName, - fileContent = fileContent, - contentType = "text/html" - ) - val rootFolderId = data.courseRootFolders[course1.id]!!.id - val fileFolder = data.folderFiles[rootFolderId]?.find { it.id == fileId } - data.addItemToModule( - course = course1, - moduleId = module.id, - item = fileFolder!! - ) - - // Create an external URL and add it as a module item - data.addItemToModule( - course = course1, - moduleId = module.id, - item = externalUrl - ) - - // Create a quiz and add it as a module item - quiz = data.addQuizToCourse( - course = course1 - ) - - data.addQuestionToQuiz( - course = course1, - quizId = quiz!!.id, - questionName = "Math 1", - questionText = "What is 2 + 5?", - questionType = "multiple_choice_question", - answers = arrayOf( - QuizAnswer(answerText = "7"), - QuizAnswer(answerText = "25"), - QuizAnswer(answerText = "-7") - ) - ) - - data.addQuestionToQuiz( - course = course1, - quizId = quiz!!.id, - questionName = "Math 2", - questionText = "Pi is greater than the square root of 2", - questionType = "true_false_question" - ) - - data.addQuestionToQuiz( - course = course1, - quizId = quiz!!.id, - questionName = "Math 3", - questionText = "Write an essay on why math is so awesome", - questionType = "essay_question" - ) - - data.addItemToModule( - course = course1, - moduleId = module.id, - item = quiz!! - ) - - val ltiTool = data.addLTITool("Google Drive", "http://google.com", course1, 1234L) - data.addItemToModule( - course = course1, - moduleId = module.id, - item = ltiTool!! - ) - // Sign in val student = data.students[0] val token = data.tokenFor(student)!! diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt index 67523c82c9..3a28429766 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/NotificationInteractionTest.kt @@ -132,23 +132,23 @@ class NotificationInteractionTest : StudentTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false) fun testNotificationList_showGradeUpdatedIfRestricted_points() { - val grade = "10.0" + val grade = "15.0" val data = goToNotifications( restrictQuantitativeData = true, gradingType = Assignment.GradingType.POINTS, - score = 10.0, + score = 15.0, grade = grade ) val assignment = data.assignments.values.first() - notificationPage.assertGradeUpdated(assignment.name!!) + notificationPage.assertHasGrade(assignment.name!!, "C") } @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.NOTIFICATIONS, TestCategory.INTERACTION, false) - fun testNotificationList_showGradeUpdatedIfRestricted_percent() { - val grade = "10%" + fun testNotificationList_convertGradeIfRestricted_percent() { + val grade = "50%" val data = goToNotifications( restrictQuantitativeData = true, gradingType = Assignment.GradingType.PERCENT, @@ -158,7 +158,7 @@ class NotificationInteractionTest : StudentTest() { val assignment = data.assignments.values.first() - notificationPage.assertGradeUpdated(assignment.name!!) + notificationPage.assertHasGrade(assignment.name!!, "F") } @Test @@ -236,13 +236,24 @@ class NotificationInteractionTest : StudentTest() { val course = data.courses.values.first() val student = data.students.first() - data.courses[course.id] = course.copy(settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData)) + val gradingScheme = listOf( + listOf("A", 0.9), + listOf("B", 0.8), + listOf("C", 0.7), + listOf("D", 0.6), + listOf("F", 0.0) + ) + + data.courses[course.id] = course.copy( + settings = CourseSettings(restrictQuantitativeData = restrictQuantitativeData), + gradingSchemeRaw = gradingScheme) repeat(numSubmissions) { val assignment = data.addAssignment( courseId = course.id, submissionType = Assignment.SubmissionType.ONLINE_TEXT_ENTRY, - gradingType = Assignment.gradingTypeToAPIString(gradingType).orEmpty() + gradingType = Assignment.gradingTypeToAPIString(gradingType).orEmpty(), + pointsPossible = 20 ) val submission = data.addSubmissionForAssignment( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt index 75b100e24b..f19c657307 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ShareExtensionInteractionTest.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import com.instructure.canvas.espresso.Stub +import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addAssignment import com.instructure.canvas.espresso.mockCanvas.init @@ -85,6 +86,8 @@ class ShareExtensionInteractionTest : StudentTest() { } @Test + @StubTablet("Stubbed in Tablet because on Firebase it's breaking the workflow while actually no tests cases has failed." + + "Once the reason will be figured out, we will put back this test to tablet as well.") fun addAndRemoveFileFromFileUploadDialog() { val data = createMockData() val student = data.students[0] 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/AssignmentDetailsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt index 2abb658dbb..5164267d7f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentDetailsPage.kt @@ -42,6 +42,7 @@ import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus import com.instructure.espresso.page.waitForView import com.instructure.espresso.page.waitForViewWithText import com.instructure.espresso.page.withAncestor @@ -50,6 +51,7 @@ import com.instructure.espresso.page.withParent import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeDown +import com.instructure.espresso.swipeUp import com.instructure.espresso.typeText import com.instructure.espresso.waitForCheck import com.instructure.student.R @@ -121,8 +123,9 @@ open class AssignmentDetailsPage : BasePage(R.id.assignmentDetailsPage) { } fun assertAssignmentLocked() { + if(CanvasTest.isLandscapeDevice()) onView(withId(R.id.swipeRefreshLayout) + withAncestor(R.id.assignmentDetailsPage)).swipeUp() onView(withId(R.id.lockedMessageTextView)).assertDisplayed() - onView(withId(R.id.lockedMessageTextView)).check(matches(containsTextCaseInsensitive("this assignment is locked"))) + onView(withId(R.id.lockedMessageTextView)).check(matches(containsTextCaseInsensitive("this assignment is locked by the module"))) } fun refresh() { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt index bb5a6ef6b5..089ac62d90 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/AssignmentListPage.kt @@ -100,12 +100,6 @@ class AssignmentListPage(val searchable: Searchable) : BasePage(pageResId = R.id onView(withId(R.id.points) + withParent(hasSibling(pointsMatcher))).assertHasText(gradeString) } - fun assertAssignmentDisplayedWithoutGrade(assignmentName: String) { - onView(withId(R.id.title) + withParent(R.id.textContainer) + withText(assignmentName)).assertDisplayed() - val pointsMatcher = withId(R.id.title) + withText(assignmentName) - onView(withId(R.id.points) + withParent(hasSibling(pointsMatcher))).assertNotDisplayed() - } - fun assertAssignmentNotDisplayed(assignmentName: String) { onView(withText(assignmentName) + withId(R.id.title) + hasSibling(withId(R.id.description))).check(doesNotExist()) } 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 54c6b73b47..6d089573a3 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 @@ -17,12 +17,10 @@ package com.instructure.student.ui.pages import androidx.appcompat.widget.AppCompatButton +import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.* +import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.espresso.assertDisplayed @@ -65,6 +63,7 @@ class BookmarkPage : BasePage() { onView(withId(R.id.bookmarkEditText)).typeText(newName) // Save + if(CanvasTest.isLandscapeDevice()) Espresso.pressBack() //need to remove soft-keyboard on landscape devices onView(allOf(isAssignableFrom(AppCompatButton::class.java), containsTextCaseInsensitive("DONE"))).click() } 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 4fcb1ef161..3261c98c36 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 @@ -87,12 +87,6 @@ class CourseGradesPage : BasePage(R.id.courseGradesPage) { onView(withId(R.id.points) + hasSibling(siblingMatcher)).assertHasText(gradeString) } - fun assertAssignmentDisplayedWithoutGrade(name: 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)).assertNotDisplayed() - } - // 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. 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 229d1253ee..caa43663b0 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 @@ -279,6 +279,15 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(matcher).check(doesNotExist()) } + fun assertGroupNotDisplayed(group: Group) { + val matcher = allOf( + withText(group.name), + withId(R.id.titleTextView), + withAncestor(R.id.swipeRefreshLayout) + ) + onView(matcher).check(doesNotExist()) + } + fun changeCourseNickname(changeTo: String) { onView(withId(R.id.newCourseNickname)).replaceText(changeTo) onView(withText(android.R.string.ok) + withAncestor(R.id.buttonPanel)).click() 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/LeftSideNavigationDrawerPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/LeftSideNavigationDrawerPage.kt index b0680bbb85..9842cbf0bc 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 @@ -8,6 +8,7 @@ import androidx.test.espresso.ViewAction import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withText +import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel @@ -21,7 +22,10 @@ import com.instructure.espresso.page.onView import com.instructure.espresso.page.onViewWithId import com.instructure.espresso.page.onViewWithText import com.instructure.espresso.page.waitForViewWithId +import com.instructure.espresso.page.withId import com.instructure.espresso.scrollTo +import com.instructure.espresso.swipeDown +import com.instructure.espresso.swipeUp import com.instructure.student.R import org.hamcrest.CoreMatchers import org.hamcrest.Matcher @@ -132,6 +136,8 @@ class LeftSideNavigationDrawerPage: BasePage() { userEmail.assertDisplayed() settings.assertDisplayed() + + if(CanvasTest.isLandscapeDevice() || CanvasTest.isLowResDevice()) onView(withId(R.id.navigationDrawer)).swipeUp() changeUser.assertDisplayed() logoutButton.assertDisplayed() @@ -144,10 +150,12 @@ class LeftSideNavigationDrawerPage: BasePage() { } private fun assertDefaultNavigationBehaviorMenuItems() { + if(CanvasTest.isLandscapeDevice()) onView(withId(R.id.navigationDrawer)).swipeDown() files.assertDisplayed() bookmarks.assertDisplayed() settings.assertDisplayed() + if(CanvasTest.isLandscapeDevice()) onView(withId(R.id.navigationDrawer)).swipeUp() showGrades.assertDisplayed() colorOverlay.assertDisplayed() @@ -157,13 +165,12 @@ class LeftSideNavigationDrawerPage: BasePage() { } private fun assertElementaryNavigationBehaviorMenuItems() { - files.assertDisplayed() bookmarks.assertNotDisplayed() - settings.assertDisplayed() - showGrades.assertNotDisplayed() colorOverlay.assertNotDisplayed() + files.assertDisplayed() + settings.assertDisplayed() help.assertDisplayed() changeUser.assertDisplayed() logoutButton.assertDisplayed() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt index 5bd887a0cb..6801a10cf7 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/NewMessagePage.kt @@ -24,14 +24,30 @@ import androidx.test.espresso.ViewAssertion import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers -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.withChild +import androidx.test.espresso.matcher.ViewMatchers.withText import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GroupApiModel -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.RecyclerViewItemCountAssertion +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithId +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withDescendant +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo +import com.instructure.espresso.typeText import com.instructure.student.R import junit.framework.Assert.assertTrue import org.hamcrest.Matchers.allOf @@ -130,7 +146,7 @@ class NewMessagePage : BasePage() { fun setMessage(messageText: String) { Espresso.closeSoftKeyboard() - onView(allOf(withId(R.id.message), hasSibling(withId(R.id.sendIndividualDivider)))) + onView(allOf(withId(R.id.message), withAncestor(R.id.messageContainer))) .scrollTo() .typeText(messageText) } 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/SyllabusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt index e0b55f5c17..df6acc3af8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt @@ -17,15 +17,23 @@ package com.instructure.student.ui.pages import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.web.assertion.WebViewAssertions +import androidx.test.espresso.web.sugar.Web +import androidx.test.espresso.web.webdriver.DriverAtoms +import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.espresso.assertDisplayed 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.swipeDown import com.instructure.student.R +import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf open class SyllabusPage : BasePage(R.id.syllabusPage) { @@ -39,7 +47,11 @@ open class SyllabusPage : BasePage(R.id.syllabusPage) { } fun selectSummaryTab() { - onView(containsTextCaseInsensitive("summary")).click() + onView(containsTextCaseInsensitive("summary") + withAncestor(R.id.syllabusTabLayout)).click() + } + + fun selectSyllabusTab() { + onView(containsTextCaseInsensitive("syllabus") + withAncestor(R.id.syllabusTabLayout)).click() } fun selectSummaryEvent(name: String) { @@ -50,4 +62,18 @@ open class SyllabusPage : BasePage(R.id.syllabusPage) { onView(allOf(withId(R.id.swipeRefreshLayout), withAncestor(R.id.syllabusPage))).swipeDown() } + fun assertNoTabs() { + onView(withId(R.id.syllabusTabLayout)).check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + } + + fun assertSyllabusBody(syllabusBody: String) { + Web.onWebView(withId(R.id.contentWebView) + withAncestor(R.id.syllabusPage)) + .withElement(DriverAtoms.findElement(Locator.ID, "content")) + .check( + WebViewAssertions.webMatches( + DriverAtoms.getText(), + Matchers.comparesEqualTo(syllabusBody) + ) + ) + } } 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..6000d1db3b 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 @@ -143,7 +143,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() diff --git a/apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt index 3f6173c2f7..9dca6b4615 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/GradesListRecyclerAdapter.kt @@ -34,6 +34,7 @@ import com.instructure.canvasapi2.models.CourseGrade import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.GradingPeriod import com.instructure.canvasapi2.models.GradingPeriodResponse +import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.isNullOrEmpty @@ -90,7 +91,7 @@ open class GradesListRecyclerAdapter( interface AdapterToGradesCallback { val isEdit: Boolean - fun notifyGradeChanged(courseGrade: CourseGrade?, restrictQuantitativeData: Boolean) + fun notifyGradeChanged(courseGrade: CourseGrade?, restrictQuantitativeData: Boolean, gradingScheme: List) fun setTermSpinnerState(isEnabled: Boolean) fun setIsWhatIfGrading(isWhatIfGrading: Boolean) } @@ -206,7 +207,7 @@ open class GradesListRecyclerAdapter( course.enrollments = mutableListOf(it) courseGrade = course.getCourseGradeFromEnrollment(it, false) val restrictQuantitativeData = course.settings?.restrictQuantitativeData ?: false - adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData) + adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData, course.gradingScheme) } } } catch (e: CancellationException) { @@ -274,7 +275,7 @@ open class GradesListRecyclerAdapter( val course = canvasContext as Course? courseGrade = course!!.getCourseGradeForGradingPeriodSpecificEnrollment(enrollment = enrollment) val restrictQuantitativeData = course.settings?.restrictQuantitativeData ?: false - adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData) + adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData, course.gradingScheme) // We need to update the course that the fragment is using course.addEnrollment(enrollment) } @@ -283,9 +284,11 @@ open class GradesListRecyclerAdapter( private fun updateCourseGrade() { // All grading periods and no grading periods are the same case - courseGrade = (canvasContext as Course).getCourseGrade(true) - val restrictQuantitativeData = (canvasContext as Course).settings?.restrictQuantitativeData ?: false - adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData) + val course = canvasContext as? Course + courseGrade = course?.getCourseGrade(true) + val restrictQuantitativeData = course?.settings?.restrictQuantitativeData ?: false + val gradingScheme = course?.gradingScheme ?: emptyList() + adapterToGradesCallback?.notifyGradeChanged(courseGrade, restrictQuantitativeData, gradingScheme) } private fun updateWithAllAssignments(forceNetwork: Boolean) { diff --git a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt index ff1cf533e7..8b09f7f7b5 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/NotificationListRecyclerAdapter.kt @@ -21,7 +21,7 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.managers.CourseManager.createCourseMap -import com.instructure.canvasapi2.managers.CourseManager.getCourses +import com.instructure.canvasapi2.managers.CourseManager.getCoursesWithGradingScheme import com.instructure.canvasapi2.managers.GroupManager.createGroupMap import com.instructure.canvasapi2.managers.GroupManager.getAllGroups import com.instructure.canvasapi2.managers.InboxManager.getConversation @@ -112,7 +112,7 @@ class NotificationListRecyclerAdapter( override val isPaginated get() = true override fun loadFirstPage() { - getCourses(true, coursesCallback) + getCoursesWithGradingScheme(true, coursesCallback) getAllGroups(groupsCallback, true) if (canvasContext.type == CanvasContext.Type.USER) { getUserStream(streamCallback, true) diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt index 0a39eb61f4..ff503444bc 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt @@ -58,9 +58,10 @@ abstract class AssignmentListRecyclerAdapter ( private var assignmentGroupCallback: StatusCallback>? = null override var currentGradingPeriod: GradingPeriod? = null private var apiJob: WeaveJob? = null - private var settingsJob: WeaveJob? = null + private var courseJob: WeaveJob? = null protected var assignmentGroups: List = emptyList() private var restrictQuantitativeData = false + private var gradingSchemes = listOf() var filter: AssignmentListFilter = AssignmentListFilter.ALL set(value) { @@ -132,17 +133,13 @@ abstract class AssignmentListRecyclerAdapter ( if changes are made here, check if they are needed in the other recycler adapters.*/ val course = canvasContext as Course - if (course.settings != null && !isRefresh) { - restrictQuantitativeData = course.settings?.restrictQuantitativeData ?: false + courseJob = tryWeave { + val detailedCourse = CourseManager.getCourseWithGradeAsync(canvasContext.id, isRefresh).await().dataOrNull + restrictQuantitativeData = detailedCourse?.settings?.restrictQuantitativeData ?: false + gradingSchemes = detailedCourse?.gradingScheme ?: emptyList() + loadAssignmentsData(course) + } catch { loadAssignmentsData(course) - } else { - settingsJob = tryWeave { - val settings = CourseManager.getCourseSettingsAsync(canvasContext.id, isRefresh).await().dataOrNull - restrictQuantitativeData = settings?.restrictQuantitativeData ?: false - loadAssignmentsData(course) - } catch { - loadAssignmentsData(course) - } } } @@ -202,7 +199,7 @@ abstract class AssignmentListRecyclerAdapter ( assignmentGroup: AssignmentGroup, assignment: Assignment ) { - (holder as AssignmentViewHolder).bind(context, assignment, canvasContext.backgroundColor, adapterToAssignmentsCallback, restrictQuantitativeData) + (holder as AssignmentViewHolder).bind(context, assignment, canvasContext.backgroundColor, adapterToAssignmentsCallback, restrictQuantitativeData, gradingSchemes) } override fun onBindEmptyHolder(holder: RecyclerView.ViewHolder, assignmentGroup: AssignmentGroup) { @@ -260,7 +257,7 @@ abstract class AssignmentListRecyclerAdapter ( override fun cancel() { super.cancel() apiJob?.cancel() - settingsJob?.cancel() + courseJob?.cancel() } } 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/assignmentdetails/AssignmentDetailsViewModel.kt index 0a7f57a4ac..f9c07cd306 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModel.kt @@ -92,6 +92,7 @@ class AssignmentDetailsViewModel @Inject constructor( private var dbSubmission: DatabaseSubmission? = null private var isUploading = false private var restrictQuantitativeData = false + private var gradingScheme = emptyList() var assignment: Assignment? = null private set @@ -165,6 +166,7 @@ class AssignmentDetailsViewModel @Inject constructor( try { val courseResult = courseManager.getCourseWithGradeAsync(course?.id.orDefault(), forceNetwork).await().dataOrThrow restrictQuantitativeData = courseResult.settings?.restrictQuantitativeData ?: false + gradingScheme = courseResult.gradingScheme isObserver = courseResult.enrollments?.firstOrNull { it.isObserver } != null @@ -268,11 +270,11 @@ class AssignmentDetailsViewModel @Inject constructor( ) val submissionStatusTint = if (assignment.isSubmitted) { - R.color.backgroundSuccess + R.color.textSuccess } else if (isMissing) { - R.color.backgroundDanger + R.color.textDanger } else { - R.color.backgroundDark + R.color.textDark } val submittedStatusIcon = if (assignment.isSubmitted) R.drawable.ic_complete_solid else R.drawable.ic_no @@ -441,7 +443,8 @@ class AssignmentDetailsViewModel @Inject constructor( colorKeeper.getOrGenerateColor(course), assignment, assignment.submission, - restrictQuantitativeData + restrictQuantitativeData, + gradingScheme = gradingScheme ), dueDate = due, submissionTypes = submissionTypes, @@ -475,7 +478,8 @@ class AssignmentDetailsViewModel @Inject constructor( selectedSubmission, restrictQuantitativeData, attempt?.isUploading.orDefault(), - attempt?.isFailed.orDefault() + attempt?.isFailed.orDefault(), + gradingScheme ) _data.value?.notifyPropertyChanged(BR.selectedGradeCellViewData) } 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/assignmentdetails/gradecellview/GradeCellViewData.kt index 6a0bc2a4c4..38d67e8b48 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/assignmentdetails/gradecellview/GradeCellViewData.kt @@ -3,8 +3,10 @@ package com.instructure.student.features.assignmentdetails.gradecellview import android.content.res.Resources import androidx.core.graphics.ColorUtils import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.canvasapi2.utils.convertScoreToLetterGrade import com.instructure.pandautils.utils.ThemedColor import com.instructure.pandautils.utils.getContentDescriptionForMinusGradeString import com.instructure.pandautils.utils.orDefault @@ -45,9 +47,10 @@ data class GradeCellViewData( submission: Submission?, restrictQuantitativeData: Boolean = false, uploading: Boolean = false, - failed: Boolean = false + failed: Boolean = false, + gradingScheme: List = emptyList() ): GradeCellViewData { - val hideGrades = restrictQuantitativeData && assignment?.isGradingTypeQuantitative == true && submission?.excused != true + val hideGrades = restrictQuantitativeData && assignment?.isGradingTypeQuantitative == true && submission?.excused != true && gradingScheme.isEmpty() val emptyGradeCell = assignment == null || submission == null || (submission.submittedAt == null && !submission.isGraded) @@ -74,7 +77,7 @@ data class GradeCellViewData( resources.getString(R.string.submissionStatusSuccessSubtitle) ) ) - else -> createGradedViewData(resources, courseColor, assignment!!, submission, restrictQuantitativeData) + else -> createGradedViewData(resources, courseColor, assignment!!, submission, restrictQuantitativeData, gradingScheme) } } @@ -83,7 +86,8 @@ data class GradeCellViewData( courseColor: ThemedColor, assignment: Assignment, submission: Submission, - restrictQuantitativeData: Boolean + restrictQuantitativeData: Boolean, + gradingScheme: List ): GradeCellViewData { val pointsPossibleText = NumberHelper.formatDecimal(assignment.pointsPossible, 2, true) val outOfText = if (restrictQuantitativeData) "" else resources.getString(R.string.outOfPointsAbbreviatedFormatted, pointsPossibleText) @@ -121,8 +125,11 @@ data class GradeCellViewData( ) ) } else if (restrictQuantitativeData) { - // We can only reach this branch when the grading type is GPA or letter grade, so don't need to handle any other case - val grade = submission.grade ?: "" + val grade = if (assignment.isGradingTypeQuantitative) { + convertScoreToLetterGrade(submission.score, assignment.pointsPossible, gradingScheme) + } else { + submission.grade ?: "" + } val accessibleGradeString = getContentDescriptionForMinusGradeString(grade, resources) val contentDescription = resources.getString( R.string.a11y_gradeCellContentDescriptionLetterGradeOnly, diff --git a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt index ef94c02f2d..88225b69ce 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/CourseModuleProgressionFragment.kt @@ -723,7 +723,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { val moduleItemAsset = ModuleItemAsset.fromAssetType(assetType) if (moduleItemAsset != ModuleItemAsset.MODULE_ITEM) { val newRoute = route.copy(secondaryClass = moduleItemAsset.routeClass, removePreviousScreen = true) - RouteMatcher.route(requireContext(), newRoute) + RouteMatcher.route(requireActivity(), newRoute) return@tryWeave } } diff --git a/apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt index c8b4771c17..bf7008a265 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/GradesListFragment.kt @@ -67,6 +67,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { private var isWhatIfGrading = false private var restrictQuantitativeData = false + private var gradingScheme = emptyList() private lateinit var allTermsGradingPeriod: GradingPeriod private lateinit var recyclerAdapter: GradesListRecyclerAdapter @@ -157,8 +158,7 @@ class GradesListFragment : ParentFragment(), Bookmarkable { } else { val gradeString = getGradeString( recyclerAdapter.courseGrade, - !isChecked, - restrictQuantitativeData + !isChecked ) txtOverallGrade.text = gradeString txtOverallGrade.contentDescription = getContentDescriptionForMinusGradeString(gradeString, requireContext()) @@ -210,11 +210,12 @@ class GradesListFragment : ParentFragment(), Bookmarkable { } } - override fun notifyGradeChanged(courseGrade: CourseGrade?, restrictQuantitativeData: Boolean) { + override fun notifyGradeChanged(courseGrade: CourseGrade?, restrictQuantitativeData: Boolean, gradingScheme: List) { Logger.d("Logging for Grades E2E, current total grade is: ${binding.txtOverallGrade.text}") if (!isAdded) return - val gradeString = getGradeString(courseGrade, !binding.showTotalCheckBox.isChecked, restrictQuantitativeData) this@GradesListFragment.restrictQuantitativeData = restrictQuantitativeData + this@GradesListFragment.gradingScheme = gradingScheme + val gradeString = getGradeString(courseGrade, !binding.showTotalCheckBox.isChecked) Logger.d("Logging for Grades E2E, new total grade is: $gradeString") binding.txtOverallGrade.text = gradeString binding.txtOverallGrade.contentDescription = getContentDescriptionForMinusGradeString(gradeString, requireContext()) @@ -302,23 +303,26 @@ class GradesListFragment : ParentFragment(), Bookmarkable { private fun getGradeString( courseGrade: CourseGrade?, - isFinal: Boolean, - restrictQuantitativeData: Boolean + isFinal: Boolean ): String { if (courseGrade == null) return getString(R.string.noGradeText) return if (isFinal) { - formatGrade(courseGrade.noFinalGrade, courseGrade.hasFinalGradeString(), courseGrade.finalGrade, courseGrade.finalScore, restrictQuantitativeData) + formatGrade(courseGrade.noFinalGrade, courseGrade.hasFinalGradeString(), courseGrade.finalGrade, courseGrade.finalScore) } else { - formatGrade(courseGrade.noCurrentGrade, courseGrade.hasCurrentGradeString(), courseGrade.currentGrade, courseGrade.currentScore, restrictQuantitativeData) + formatGrade(courseGrade.noCurrentGrade, courseGrade.hasCurrentGradeString(), courseGrade.currentGrade, courseGrade.currentScore) } } - private fun formatGrade(noGrade: Boolean, hasGradeString: Boolean, grade: String?, score: Double?, restrictQuantitativeData: Boolean): String { + private fun formatGrade(noGrade: Boolean, hasGradeString: Boolean, grade: String?, score: Double?): String { return if (noGrade) { getString(R.string.noGradeText) } else { if (restrictQuantitativeData) { - if (hasGradeString) grade.orEmpty() else getString(R.string.noGradeText) + when { + hasGradeString -> grade.orEmpty() + gradingScheme.isNotEmpty() && score != null -> convertPercentScoreToLetterGrade(score / 100, gradingScheme) + else -> getString(R.string.noGradeText) + } } else { NumberHelper.doubleToPercentage(score) + if (hasGradeString) String.format(" (%s)", grade) else "" } 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 6962b7d493..b4c6148bc8 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 @@ -319,7 +319,7 @@ class InboxComposeMessageFragment : ParentFragment(), FileUploadDialogParent { private fun handleExit() { // Check to see if the user has made any changes - if (binding.editSubject.text.isNotBlank() || binding.message.text.isNotBlank() || attachments.isNotEmpty()) { + if (binding.editSubject.text?.isNotBlank() == true || binding.message.text?.isNotBlank() == true || attachments.isNotEmpty()) { shouldAllowExit = false // Use childFragmentManager so that exiting the compose fragment also dismisses the dialog UnsavedChangesExitDialog.show(childFragmentManager) { diff --git a/apps/student/src/main/java/com/instructure/student/holders/AssignmentViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/AssignmentViewHolder.kt index 71355343c2..7c824ed748 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/AssignmentViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/AssignmentViewHolder.kt @@ -21,6 +21,7 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.utils.DateHelper import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.setTextForVisibility @@ -36,7 +37,8 @@ class AssignmentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { assignment: Assignment, courseColor: Int, adapterToFragmentCallback: AdapterToFragmentCallback, - restrictQuantitativeData: Boolean + restrictQuantitativeData: Boolean, + gradingSchemes: List ) = with(ViewholderCardGenericBinding.bind(itemView)) { title.text = assignment.name @@ -48,13 +50,13 @@ class AssignmentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val submission = assignment.submission // Posted At now determines if an assignment is muted, even for old gradebook - val hideGrade = restrictQuantitativeData && assignment.isGradingTypeQuantitative && submission?.excused != true + val hideGrade = restrictQuantitativeData && assignment.isGradingTypeQuantitative && submission?.excused != true && gradingSchemes.isEmpty() if (submission?.postedAt == null || hideGrade) { // Mute that score points.visibility = View.GONE } else { points.visibility = View.VISIBLE - BinderUtils.setupGradeText(context, points, assignment, submission, courseColor, restrictQuantitativeData) + BinderUtils.setupGradeText(context, points, assignment, submission, courseColor, restrictQuantitativeData, gradingSchemes) } val drawable = BinderUtils.getAssignmentIcon(assignment) 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 84932627d0..19179863a4 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 @@ -68,15 +68,17 @@ class GradeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { points.setGone() } else { val submission = assignment.submission - val restrictQuantitativeData = (canvasContext as? Course)?.settings?.restrictQuantitativeData ?: false + val course = canvasContext as? Course + val restrictQuantitativeData = course?.settings?.restrictQuantitativeData ?: false + val gradingScheme = course?.gradingScheme ?: emptyList() if (submission != null && Const.PENDING_REVIEW == submission.workflowState) { points.setGone() icon.setNestedIcon(R.drawable.ic_complete_solid, canvasContext.backgroundColor) - } else if (restrictQuantitativeData && assignment.isGradingTypeQuantitative && submission?.excused != true) { + } else if (restrictQuantitativeData && assignment.isGradingTypeQuantitative && submission?.excused != true && gradingScheme.isEmpty()) { points.setGone() } else { points.setVisible() - val (grade, contentDescription) = BinderUtils.getGrade(assignment, submission, context, restrictQuantitativeData) + val (grade, contentDescription) = BinderUtils.getGrade(assignment, submission, context, restrictQuantitativeData, gradingScheme) points.text = grade points.contentDescription = contentDescription } diff --git a/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt b/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt index 22f5f09bc2..113718dd12 100644 --- a/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt +++ b/apps/student/src/main/java/com/instructure/student/holders/NotificationViewHolder.kt @@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.StreamItem +import com.instructure.canvasapi2.utils.convertScoreToLetterGrade import com.instructure.pandautils.utils.* import com.instructure.student.R import com.instructure.student.adapter.NotificationListRecyclerAdapter @@ -99,16 +100,20 @@ class NotificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) drawableResId = R.drawable.ic_assignment icon.contentDescription = context.getString(R.string.assignmentIcon) - val restrictQuantitativeData = (item.canvasContext as? Course)?.settings?.restrictQuantitativeData.orDefault() - && item.assignment?.isGradingTypeQuantitative.orDefault() + val course = item.canvasContext as? Course + val restrictQuantitativeData = course?.settings?.restrictQuantitativeData.orDefault() + val gradingScheme = course?.gradingScheme.orEmpty() // Need to prepend "Grade" in the message if there is a valid score - if (item.score != -1.0 && !restrictQuantitativeData) { + if (item.score != -1.0) { // If the submission has a grade (like a letter or percentage) display it - if (item.grade != null - && item.grade != "" - && item.grade != "null" - ) { - description.text = context.resources.getString(R.string.grade) + ": " + item.grade + val pointsPossible = item.assignment?.pointsPossible + val grade = if (item.assignment?.isGradingTypeQuantitative == true && restrictQuantitativeData && pointsPossible != null) { + convertScoreToLetterGrade(item.score, pointsPossible, gradingScheme) + } else { + item.grade + } + if (grade != null && grade != "" && grade != "null") { + description.text = context.resources.getString(R.string.grade) + ": " + grade } else { description.text = context.resources.getString(R.string.grade) + description.text } 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 ff84ef1cc8..0d264ec66a 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 @@ -53,11 +53,7 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p override fun onTabUnselected(tab: TabLayout.Tab?) {} override fun onTabSelected(tab: TabLayout.Tab?) { - if (tab?.position == 0) { - binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusScrollView) - } else { - binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusEventsRecycler, R.id.syllabusEmptyView) - } + setupSwipeableChildren(tab?.position) } } @@ -96,18 +92,34 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p is SyllabusViewState.Loaded -> { binding.swipeRefreshLayout.isRefreshing = false + val pager = binding.syllabusPager + val hasBoth = state.eventsState != null && state.syllabus != null binding.syllabusTabLayout.setVisible(hasBoth) - binding.syllabusPager.canSwipe = hasBoth + pager.canSwipe = hasBoth - binding.syllabusPager.setCurrentItem(if (state.syllabus == null) 1 else 0, false) + pager.setCurrentItem(if (state.syllabus == null) 1 else 0, false) + + if (state.syllabus != null) webviewBinding?.syllabusWebViewWrapper?.loadHtml( + state.syllabus, + context.getString(com.instructure.pandares.R.string.syllabus) + ) - if (state.syllabus != null) webviewBinding?.syllabusWebViewWrapper?.loadHtml(state.syllabus, context.getString(com.instructure.pandares.R.string.syllabus)) if (state.eventsState != null) renderEvents(state.eventsState) + + setupSwipeableChildren(pager.currentItem) } } } + private fun setupSwipeableChildren(position: Int?) { + if (position == 0) { + binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusScrollView) + } else { + binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusEventsRecycler, R.id.syllabusEmptyView) + } + } + private fun renderEvents(eventsState: EventsViewState) { with (eventsState) { eventsBinding?.syllabusEmptyView?.setVisible(visibility.empty) @@ -115,12 +127,6 @@ class SyllabusView(val canvasContext: CanvasContext, inflater: LayoutInflater, p eventsBinding?.syllabusEventsRecycler?.setVisible(visibility.list) } - if (binding.syllabusPager.currentItem == 0) { - binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusScrollView) - } else { - binding.swipeRefreshLayout.setSwipeableChildren(R.id.syllabusEventsRecycler, R.id.syllabusEmptyView) - } - when (eventsState) { EventsViewState.Error -> { eventsBinding?.syllabusRetry?.onClick { consumer?.accept(SyllabusEvent.PullToRefresh) } diff --git a/apps/student/src/main/java/com/instructure/student/util/BinderUtils.kt b/apps/student/src/main/java/com/instructure/student/util/BinderUtils.kt index 005c4d4a7b..20af9eb4ac 100644 --- a/apps/student/src/main/java/com/instructure/student/util/BinderUtils.kt +++ b/apps/student/src/main/java/com/instructure/student/util/BinderUtils.kt @@ -23,8 +23,10 @@ import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.canvasapi2.utils.convertScoreToLetterGrade import com.instructure.canvasapi2.utils.isValid import com.instructure.canvasapi2.utils.validOrNull import com.instructure.pandautils.utils.* @@ -36,7 +38,7 @@ object BinderUtils { @Suppress("DEPRECATION") fun getHtmlAsText(html: String?) = html?.validOrNull()?.let { StringUtilities.simplifyHTML(Html.fromHtml(it)) } - fun getGrade(assignment: Assignment, submission: Submission?, context: Context, restrictQuantitativeData: Boolean = false): DisplayGrade { + fun getGrade(assignment: Assignment, submission: Submission?, context: Context, restrictQuantitativeData: Boolean, gradingScheme: List): DisplayGrade { val possiblePoints = assignment.pointsPossible val pointsPossibleText = NumberHelper.formatDecimal(possiblePoints, 2, true) @@ -108,6 +110,11 @@ object BinderUtils { } } + if (restrictQuantitativeData && assignment.isGradingTypeQuantitative) { + val letterGrade = convertScoreToLetterGrade(submission.score, assignment.pointsPossible, gradingScheme) + return DisplayGrade(letterGrade, getContentDescriptionForMinusGradeString(letterGrade, context).validOrNull() ?: letterGrade) + } + // Numeric grade submission.grade?.toDoubleOrNull()?.let { parsedGrade -> if (restrictQuantitativeData) return DisplayGrade() @@ -141,11 +148,11 @@ object BinderUtils { assignment: Assignment, submission: Submission, color: Int, - restrictQuantitativeData: Boolean + restrictQuantitativeData: Boolean, + gradingScheme: List ) { - val hasGrade = submission.grade.isValid() - val (grade, contentDescription) = getGrade(assignment, submission, context, restrictQuantitativeData) - if (hasGrade) { + val (grade, contentDescription) = getGrade(assignment, submission, context, restrictQuantitativeData, gradingScheme) + if (!submission.excused && grade.isValid()) { textView.text = grade textView.contentDescription = contentDescription textView.setTextAppearance(R.style.TextStyle_Grade) diff --git a/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt index 052d93b02a..a743dd7cc4 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/GradesViewWidgetService.kt @@ -133,7 +133,7 @@ class GradesViewWidgetService : BaseRemoteViewsService(), Serializable { if(NetworkUtils.isNetworkAvailable && ApiPrefs.user != null) { try { // Force network so we always get the latest data for grades - setData(CourseManager.getCoursesSynchronous(true).filter + setData(CourseManager.getCoursesSynchronousWithGradingScheme(true).filter { it.isFavorite && it.isCurrentEnrolment() && !it.isInvited() }) } catch (e: Throwable) { Logger.e("Could not load " + this::class.java.simpleName + " widget. " + e.message) diff --git a/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt index 566a268e98..a012f9f8fc 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/NotificationViewWidgetService.kt @@ -34,7 +34,6 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.StreamItem import com.instructure.canvasapi2.utils.* -import com.instructure.pandautils.utils.ColorKeeper import com.instructure.student.R import com.instructure.student.activity.NotificationWidgetRouter import com.instructure.student.util.StringUtilities @@ -88,8 +87,10 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable { if (!BaseRemoteViewsService.shouldHideDetails(appWidgetId)) { val restrictQuantitativeData = (streamItem.canvasContext as? Course)?.settings?.restrictQuantitativeData ?: false - if (streamItem.getMessage(ContextKeeper.appContext, restrictQuantitativeData) != null) { - row.setTextViewText(R.id.message, StringUtilities.simplifyHTML(Html.fromHtml(streamItem.getMessage(ContextKeeper.appContext, restrictQuantitativeData), Html.FROM_HTML_MODE_LEGACY))) + val gradingScheme = (streamItem.canvasContext as? Course)?.gradingScheme ?: emptyList() + val message = streamItem.getMessage(ContextKeeper.appContext, restrictQuantitativeData, gradingScheme) + if (message != null) { + row.setTextViewText(R.id.message, StringUtilities.simplifyHTML(Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY))) row.setTextColor(R.id.message, BaseRemoteViewsService.getWidgetSecondaryTextColor(appWidgetId, applicationContext)) } else { row.setTextViewText(R.id.message, "") @@ -140,7 +141,7 @@ class NotificationViewWidgetService : BaseRemoteViewsService(), Serializable { override fun loadData() { if(NetworkUtils.isNetworkAvailable && ApiPrefs.user != null) { try { - val courses = CourseManager.getCoursesSynchronous(true) + val courses = CourseManager.getCoursesSynchronousWithGradingScheme(true) .filter { it.isFavorite && !it.accessRestrictedByDate && !it.isInvited() } val groups = GroupManager.getFavoriteGroupsSynchronous(false) val userStream = StreamManager.getUserStreamSynchronous(25, true).toMutableList() diff --git a/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt b/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt index 0a74062e27..14def809cb 100644 --- a/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt +++ b/apps/student/src/main/java/com/instructure/student/widget/TodoViewWidgetService.kt @@ -35,7 +35,6 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.ScheduleItem import com.instructure.canvasapi2.models.ToDo import com.instructure.canvasapi2.utils.* -import com.instructure.pandautils.utils.ColorKeeper import java.io.Serializable import java.util.* @@ -161,7 +160,7 @@ class TodoViewWidgetService : BaseRemoteViewsService(), Serializable { override fun loadData() { if(NetworkUtils.isNetworkAvailable && ApiPrefs.user != null) { try { - val courses = CourseManager.getCoursesSynchronous(true) + val courses = CourseManager.getCoursesSynchronousWithGradingScheme(true) .filter { it.isFavorite && !it.accessRestrictedByDate && !it.isInvited() } val groups = GroupManager.getFavoriteGroupsSynchronous(true) val todos = ToDoManager.getTodosSynchronous(ApiPrefs.user!!, true) diff --git a/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml b/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml index 16b4e10ed5..6a623ffdbb 100644 --- a/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml +++ b/apps/student/src/main/res/layout/fragment_inbox_compose_message.xml @@ -120,22 +120,26 @@ - - + android:layout_height="wrap_content"> + + + - + android:layout_height="wrap_content"> + + + diff --git a/apps/student/src/main/res/layout/fragment_unsupported_file_type.xml b/apps/student/src/main/res/layout/fragment_unsupported_file_type.xml index 202c4d34cd..eb03f71252 100644 --- a/apps/student/src/main/res/layout/fragment_unsupported_file_type.xml +++ b/apps/student/src/main/res/layout/fragment_unsupported_file_type.xml @@ -48,7 +48,7 @@ android:layout_height="64dp" android:importantForAccessibility="no" android:scaleType="fitCenter" - android:tint="@color/textLight" + app:tint="@color/textDark" app:srcCompat="@drawable/ic_canvas_logo_red"/> @color/textDark + + diff --git a/apps/student/src/main/res/values/themes_canvastheme.xml b/apps/student/src/main/res/values/themes_canvastheme.xml index 25ff54c211..286ae42ffc 100755 --- a/apps/student/src/main/res/values/themes_canvastheme.xml +++ b/apps/student/src/main/res/values/themes_canvastheme.xml @@ -43,6 +43,7 @@ @style/ModalDialogStyle @style/AnnotationNoteHinter @color/backgroundLight + @style/DatePickerStyle + + 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 2832736e43..dbbfd36533 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 @@ -82,7 +82,8 @@ object CoursesApi { fun createCourse( enrollmentTermId: Long? = null, publish: Boolean = true, - coursesService: CoursesService = adminCoursesService + coursesService: CoursesService = adminCoursesService, + syllabusBody: String? = null ): CourseApiModel { val randomCourseName = Randomizer.randomCourseName() val course = CreateCourseWrapper( @@ -90,7 +91,8 @@ object CoursesApi { course = CreateCourse( name = randomCourseName, courseCode = randomCourseName.substring(0, 2), - enrollmentTermId = enrollmentTermId + enrollmentTermId = enrollmentTermId, + syllabusBody = syllabusBody ) ) return coursesService diff --git a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SeedApi.kt b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SeedApi.kt index 9536d92f91..98406ac1d3 100644 --- a/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SeedApi.kt +++ b/automation/dataseedingapi/src/main/kotlin/com/instructure/dataseeding/api/SeedApi.kt @@ -16,7 +16,11 @@ package com.instructure.dataseeding.api -import com.instructure.dataseeding.model.* +import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.dataseeding.model.DiscussionApiModel +import com.instructure.dataseeding.model.EnrollmentApiModel +import com.instructure.dataseeding.model.FavoriteApiModel // Data-seeding API object SeedApi { @@ -172,7 +176,7 @@ object SeedApi { with(seededData) { for (c in 0 until maxOf(request.courses + request.pastCourses, request.favoriteCourses)) { // Seed course - addCourses(createCourse(request.gradingPeriods, request.publishCourses)) + addCourses(createCourse(request.gradingPeriods, request.publishCourses, syllabusBody = request.syllabusBody)) // Seed users for (t in 0 until request.teachers) { @@ -316,7 +320,7 @@ object SeedApi { return if (accountId != null) { CoursesApi.createCourseInSubAccount(accountId = accountId, homeroomCourse = isHomeroomCourse, enrollmentTermId = enrollmentTermId, publish = publishCourses, syllabusBody = syllabusBody) } else { - CoursesApi.createCourse(enrollmentTermId, publishCourses) + CoursesApi.createCourse(enrollmentTermId, publishCourses, syllabusBody = syllabusBody) } } } \ No newline at end of file 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/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/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 0a57121bbe..9ebfd03b96 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 @@ -42,6 +42,9 @@ object CourseAPI { @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") suspend fun getFirstPageCourses(@Tag params: RestParams): DataResult> @@ -51,7 +54,7 @@ object CourseAPI { @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") val firstPageCoursesWithConcluded: Call> - @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") + @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:GET("courses?include[]=term&include[]=syllabus_body&include[]=license&include[]=is_public&include[]=permissions&enrollment_state=active") @@ -72,7 +75,7 @@ object CourseAPI { @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[]=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") + @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?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") @@ -126,8 +129,8 @@ object CourseAPI { } @Throws(IOException::class) - fun getCoursesSynchronously(adapter: RestBuilder, params: RestParams): List? { - val firstPageResponse = adapter.build(CoursesInterface::class.java, params).firstPageCourses.execute() + fun getCoursesSynchronouslyWithGradingScheme(adapter: RestBuilder, params: RestParams): List? { + val firstPageResponse = adapter.build(CoursesInterface::class.java, params).firstPageCoursesWithGradingScheme.execute() return getCoursesRecursive(adapter, params, firstPageResponse, firstPageResponse.body()) } @@ -169,6 +172,10 @@ object CourseAPI { callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCourses).enqueue(callback) } + fun getFirstPageCoursesWithGradingScheme(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { + callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCoursesWithGradingScheme).enqueue(callback) + } + fun getFirstPageCoursesWithConcluded(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCoursesWithConcluded).enqueue(callback) } 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 5283bade91..ea621abb56 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 @@ -69,6 +69,25 @@ object CourseManager { CourseAPI.getFirstPageCourses(adapter, depaginatedCallback, params) } + fun getCoursesWithGradingScheme(forceNetwork: Boolean, callback: StatusCallback>) { + if (ApiPrefs.isStudentView) { + getCoursesTeacher(forceNetwork, callback) + return + } + + 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.getFirstPageCoursesWithGradingScheme(adapter, depaginatedCallback, params) + } + fun getCoursesWithConcluded(forceNetwork: Boolean, callback: StatusCallback>) { if (ApiPrefs.isStudentView) { getCoursesTeacher(forceNetwork, callback) @@ -341,11 +360,11 @@ object CourseManager { } @Throws(IOException::class) - fun getCoursesSynchronous(forceNetwork: Boolean): List { + fun getCoursesSynchronousWithGradingScheme(forceNetwork: Boolean): List { val adapter = RestBuilder() val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) - val data = CourseAPI.getCoursesSynchronously(adapter, params) + val data = CourseAPI.getCoursesSynchronouslyWithGradingScheme(adapter, params) return data ?: ArrayList() } 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 81e6665bbd..13a694a693 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 @@ -23,6 +23,7 @@ import com.instructure.canvasapi2.utils.isCreationPending import com.instructure.canvasapi2.utils.isNullOrEmpty import com.instructure.canvasapi2.utils.toDate import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue import java.util.* @Parcelize @@ -80,7 +81,9 @@ data class Course( @SerializedName("grading_periods") val gradingPeriods: List? = null, @SerializedName("settings") - val settings: CourseSettings? = null + val settings: CourseSettings? = null, + @SerializedName("grading_scheme") + val gradingSchemeRaw: List>? = null, ) : CanvasContext(), Comparable { override val type: Type get() = Type.COURSE @@ -128,6 +131,17 @@ data class Course( return false } + val gradingScheme: List + get() { + return gradingSchemeRaw?.map { row -> + if (row.size < 2 || row[0] !is String || row[1] !is Double) { + null + } else { + GradingSchemeRow(row[0] as String, row[1] as Double) + } + }?.filterNotNull()?.sortedByDescending { it.value } ?: emptyList() + } + /** * A helper method to get access to all course grade values in one place and how to display them * diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/GradingSchemeRow.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/GradingSchemeRow.kt new file mode 100644 index 0000000000..cdab07e260 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/GradingSchemeRow.kt @@ -0,0 +1,19 @@ +/* + * 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.canvasapi2.models + +data class GradingSchemeRow(val name: String, val value: Double) \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/StreamItem.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/StreamItem.kt index 7e50e7e874..dc5a269441 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/StreamItem.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/StreamItem.kt @@ -21,6 +21,7 @@ import android.content.Context import com.google.gson.annotations.SerializedName import com.instructure.canvasapi2.R import com.instructure.canvasapi2.utils.APIHelper +import com.instructure.canvasapi2.utils.convertScoreToLetterGrade import com.instructure.canvasapi2.utils.toDate import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -178,9 +179,9 @@ data class StreamItem( return title } - fun getMessage(context: Context, restrictQuantitativeData: Boolean = false): String? { + fun getMessage(context: Context, restrictQuantitativeData: Boolean = false, gradingScheme: List = emptyList()): String? { if (message == null) { - message = createMessage(context, restrictQuantitativeData) + message = createMessage(context, restrictQuantitativeData, gradingScheme) } return message } @@ -214,7 +215,7 @@ data class StreamItem( } } - private fun createMessage(context: Context, restrictQuantitativeData: Boolean = false): String? { + private fun createMessage(context: Context, restrictQuantitativeData: Boolean = false, gradingScheme: List = emptyList()): String? { when (getStreamItemType()) { StreamItem.Type.CONVERSATION -> { if (conversation == null) { @@ -234,7 +235,7 @@ data class StreamItem( val displayedGrade = when { excused -> context.getString(R.string.gradeExcused) - restrictQuantitativeData -> getGradeWhenQuantitativeDataRestricted(context) + restrictQuantitativeData -> getGradeWhenQuantitativeDataRestricted(context, gradingScheme, score, assignment?.pointsPossible) score != -1.0 -> score.toString().orEmpty() else -> "" } @@ -255,9 +256,13 @@ data class StreamItem( } else message } - private fun getGradeWhenQuantitativeDataRestricted(context: Context): String { + private fun getGradeWhenQuantitativeDataRestricted(context: Context, gradingScheme: List, score: Double, maxScore: Double?): String { return if (assignment?.isGradingTypeQuantitative == true) { - context.getString(R.string.gradeUpdated) + if (gradingScheme.isEmpty() || maxScore == null) { + context.getString(R.string.gradeUpdated) + } else { + convertScoreToLetterGrade(score, maxScore, gradingScheme) + } } else { grade.orEmpty() } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ModelExtensions.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ModelExtensions.kt index dd75aaccc5..80485eddc5 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ModelExtensions.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/ModelExtensions.kt @@ -174,4 +174,16 @@ fun Assignment.toScheduleItem() : ScheduleItem { assignment = this, lockedModuleName = lockInfo?.lockedModuleName ).apply { courseId = id } +} + +fun convertScoreToLetterGrade(score: Double, maxScore: Double, gradingScheme: List): String { + if (maxScore == 0.0) return "" + val percent = (score / maxScore) + return convertPercentScoreToLetterGrade(percent, gradingScheme) +} + +fun convertPercentScoreToLetterGrade(percentScore: Double, gradingScheme: List): String { + if (gradingScheme.isEmpty()) return "" + val grade = gradingScheme.firstOrNull { percentScore >= it.value } ?: gradingScheme.last() + return grade.name } \ No newline at end of file diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt index 593eced3a1..17b9a342ab 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/pact/canvas/apis/CoursesApiPactTests.kt @@ -210,7 +210,7 @@ class CoursesApiPactTests : ApiPactTestBase() { //region Test grabbing a single course with a grade // - val courseWithGradeQuery = "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" + val courseWithGradeQuery = "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" val courseWithGradePath = "/api/v1/courses/3" val courseWithGradeFieldInfo = PactCourseFieldConfig.fromQueryString(courseId = 3, isFavorite = true, query = courseWithGradeQuery) val courseWithGradeResponseBody = LambdaDsl.newJsonBody { obj -> diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt index 6e0cd09412..94284e772f 100644 --- a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/unit/CourseTest.kt @@ -19,6 +19,7 @@ package com.instructure.canvasapi2.unit import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.GradingSchemeRow import com.instructure.canvasapi2.models.Section import com.instructure.canvasapi2.models.Term import com.instructure.canvasapi2.utils.Logger @@ -562,4 +563,22 @@ class CourseTest { assertTrue(course.isBetweenValidDateRange()) } + + @Test + fun `Grading schemes are sorted and filtered correctly`() { + val course = baseCourse.copy(gradingSchemeRaw = listOf( + listOf("A", 0.95), + listOf("C", 0.7), + listOf(0.8), + listOf("B", 0.9), + listOf("A", "B") + )) + + val gradingSchemes = course.gradingScheme + + assertEquals(3, gradingSchemes.size) + assertEquals(GradingSchemeRow("A", 0.95), gradingSchemes[0]) + assertEquals(GradingSchemeRow("B", 0.9), gradingSchemes[1]) + assertEquals(GradingSchemeRow("C", 0.7), gradingSchemes[2]) + } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/ModelExtensionsTest.kt b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/ModelExtensionsTest.kt new file mode 100644 index 0000000000..c50f1307f0 --- /dev/null +++ b/libs/canvas-api-2/src/test/java/com/instructure/canvasapi2/utils/ModelExtensionsTest.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.canvasapi2.utils + +import com.instructure.canvasapi2.models.GradingSchemeRow +import org.junit.Assert +import org.junit.Assert.* +import org.junit.Test + +class ModelExtensionsTest { + + private val gradingSchemes = listOf( + GradingSchemeRow("A", 0.95), + GradingSchemeRow("B", 0.9), + GradingSchemeRow("C", 0.8), + GradingSchemeRow("D", 0.7), + GradingSchemeRow("F", 0.6) + ) + + @Test + fun `Score to letter grade returns empty string when max score is 0`() { + val result = convertScoreToLetterGrade(1.0, 0.0, gradingSchemes) + + assertEquals("", result) + } + + @Test + fun `Score to letter grade returns empty string when grading schemes is empty`() { + val result = convertScoreToLetterGrade(1.0, 10.0, emptyList()) + + assertEquals("", result) + } + + @Test + fun `Score to letter grade returns last item when nothing matches`() { + val result = convertScoreToLetterGrade(1.0, 10.0, gradingSchemes) + + assertEquals("F", result) + } + + @Test + fun `Score to letter grade returns last item when zero points`() { + val result = convertScoreToLetterGrade(0.0, 10.0, gradingSchemes) + + assertEquals("F", result) + } + + @Test + fun `Score to letter grade returns first item when overgraded`() { + val result = convertScoreToLetterGrade(15.0, 10.0, gradingSchemes) + + assertEquals("A", result) + } + + @Test + fun `Score to letter grade returns items inclusively`() { + val result = convertScoreToLetterGrade(95.0, 100.0, gradingSchemes) + + assertEquals("A", result) + } + + @Test + fun `Score to letter grade returns correct items with more decimal places`() { + val result = convertScoreToLetterGrade(94.9999, 100.0, gradingSchemes) + + assertEquals("B", result) + } + + @Test + fun `Score to letter grade returns the last value with negatice score`() { + val result = convertScoreToLetterGrade(-50.0, 100.0, gradingSchemes) + + assertEquals("F", result) + } + + @Test + fun `Score to letter grade returns C for 80 percent`() { + val result = convertScoreToLetterGrade(80.0, 100.0, gradingSchemes) + + assertEquals("C", result) + } + + @Test + fun `Score to letter grade returns D for 70 percent`() { + val result = convertScoreToLetterGrade(70.0, 100.0, gradingSchemes) + + assertEquals("D", result) + } + + @Test + fun `Convert percent to letter grade returns empty string when no grading schemes is empty`() { + val result = convertPercentScoreToLetterGrade(1.0, emptyList()) + + assertEquals("", result) + } + + @Test + fun `Convert percent to letter grade returns last item when nothing matches`() { + val result = convertPercentScoreToLetterGrade( 0.1, gradingSchemes) + + assertEquals("F", result) + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en.arb index 428ebdf814..451f221a09 100644 --- a/libs/flutter_student_embed/lib/l10n/res/intl_en.arb +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:56.872320", + "@@last_modified": "2023-08-25T11:04:30.842905", "coursesLabel": "Courses", "@coursesLabel": { "description": "The label for the Courses tab", diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb b/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb index 428ebdf814..451f221a09 100644 --- a/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb +++ b/libs/flutter_student_embed/lib/l10n/res/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-04-14T11:04:56.872320", + "@@last_modified": "2023-08-25T11:04:30.842905", "coursesLabel": "Courses", "@coursesLabel": { "description": "The label for the Courses tab", 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 c1b7f0687f..d236a8a53b 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 @@ -20,6 +20,7 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Intent import android.content.pm.ApplicationInfo +import android.content.res.ColorStateList import android.os.Bundle import android.view.GestureDetector import android.view.MotionEvent @@ -62,6 +63,7 @@ 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.* @@ -220,7 +222,7 @@ abstract class BaseLoginLandingPageActivity : AppCompatActivity(), ErrorReportDi ColorUtils.colorIt(color, canvasLogo) // App Name/Type. Will not be present in all layout versions - appDescriptionType.setTextColor(color) + canvasWordmark.imageTintList = ColorStateList.valueOf(color) appDescriptionType.setText(appTypeName()) ViewStyler.themeStatusBar(this@BaseLoginLandingPageActivity) diff --git a/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml b/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml index a2b8b7a589..a9de871d69 100644 --- a/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml +++ b/libs/login-api-2/src/main/res/layout-land/activity_login_landing_page.xml @@ -55,7 +55,7 @@ android:layout_marginBottom="2dp" android:adjustViewBounds="true" android:importantForAccessibility="no" - android:tint="@color/tiara" + android:tint="@color/login_teacherAppTheme" app:srcCompat="@drawable/ic_canvas_wordmark" /> diff --git a/libs/login-api-2/src/main/res/layout/activity_login_landing_page.xml b/libs/login-api-2/src/main/res/layout/activity_login_landing_page.xml index b41bb274f1..d1e37b99ca 100644 --- a/libs/login-api-2/src/main/res/layout/activity_login_landing_page.xml +++ b/libs/login-api-2/src/main/res/layout/activity_login_landing_page.xml @@ -55,8 +55,8 @@ android:layout_marginTop="24dp" android:layout_marginBottom="2dp" android:adjustViewBounds="true" - android:importantForAccessibility="no" - android:tint="@color/tiara" + android:contentDescription="@string/canvas" + android:tint="@color/login_teacherAppTheme" app:srcCompat="@drawable/ic_canvas_wordmark" /> diff --git a/libs/login-api-2/src/main/res/layout/adapter_account_footer.xml b/libs/login-api-2/src/main/res/layout/adapter_account_footer.xml index d42dbae34d..380e298d7c 100644 --- a/libs/login-api-2/src/main/res/layout/adapter_account_footer.xml +++ b/libs/login-api-2/src/main/res/layout/adapter_account_footer.xml @@ -44,6 +44,6 @@ android:textColor="@color/textInfo" android:fontFamily="@font/lato_font_family" android:textSize="16sp" - android:text="@string/accountDomainFooterLink"/> + android:text="@string/accountDomainFooterLoginHelp"/> \ No newline at end of file diff --git a/libs/login-api-2/src/main/res/values/strings.xml b/libs/login-api-2/src/main/res/values/strings.xml index 4acc7c102b..493fa8837d 100644 --- a/libs/login-api-2/src/main/res/values/strings.xml +++ b/libs/login-api-2/src/main/res/values/strings.xml @@ -112,7 +112,7 @@ F school.instructure.com Remove Previous User Can\'t find your school? Try typing the full school URL. - Tap here for help. + Tap here for login help. No Internet Connection This action requires an internet connection. diff --git a/libs/pandares/src/main/res/drawable/ic_rating_star.xml b/libs/pandares/src/main/res/drawable/ic_rating_star.xml new file mode 100644 index 0000000000..0c50640422 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_rating_star.xml @@ -0,0 +1,5 @@ + + + diff --git a/libs/pandares/src/main/res/drawable/ic_rating_star_outline.xml b/libs/pandares/src/main/res/drawable/ic_rating_star_outline.xml new file mode 100644 index 0000000000..c32749453d --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_rating_star_outline.xml @@ -0,0 +1,5 @@ + + + diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index 81c3003445..6aefe0cf01 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -455,6 +455,7 @@ تم الحذف + تم تحديث الدرجة جارٍ تحميل محتوى Canvas… UnknownDevice @@ -1170,6 +1171,7 @@ يمكنك فتح تفاصيل الإرسال من هنا + الدرجة: %s %s من الدقائق %s دقيقة diff --git a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml index e1651e56b6..3d89c5ace5 100644 --- a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml @@ -432,6 +432,7 @@ Slettet + Vurdering opdateret Indlæser Canvas-indhold… UnknownDevice @@ -1119,6 +1120,7 @@ Du kan åbne afleveringsdetaljerne herfra + Vurdering: %s %s minut %s minutter diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index 9d91414183..013c458795 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -432,6 +432,7 @@ Deleted + Grade updated Loading Canvas Content… UnknownDevice @@ -1119,6 +1120,7 @@ You can open Submission details from here + Grade: %s %s Minute %s Minutes diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml index 4072036e2e..e860a4f857 100644 --- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -432,6 +432,7 @@ Deleted + Grade updated Loading Canvas Content… UnknownDevice @@ -1119,6 +1120,7 @@ You can open Submission details from here + Grade: %s %s minute %s Minutes diff --git a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml index bd6ae8ec18..3bbbfdbd80 100644 --- a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -432,6 +432,7 @@ Slettet + Vurdering oppdatert Laster Canvas-innhold… UnknownDevice @@ -1119,6 +1120,7 @@ Du kan åpne Detaljer om innlevering her + Vurdering: %s %s minutt %s minutter diff --git a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml index 44ae3284e8..eb7b807e44 100644 --- a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -432,6 +432,7 @@ Borttagen + Bedömning uppdaterat Läser in Canvas-innehåll… UnknownDevice @@ -1119,6 +1120,7 @@ Du kan öppna inlämningsinformationen härifrån + Bedömning: %s %s minut %s minuter diff --git a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml index b6bf168689..ee91fbdcd0 100644 --- a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml @@ -426,6 +426,7 @@ 已刪除 + 評分已更新 載入 Canvas 內容… UnknownDevice @@ -1106,6 +1107,7 @@ 您可以從此處開啟提交項目詳細資料 + 評分:%s %s 分鐘 diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml index 815e304d01..9fd0ff9c21 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml @@ -426,6 +426,7 @@ 已删除 + 评分已更新 正在加载Canvas内容… UnknownDevice @@ -1106,6 +1107,7 @@ 您可以从此处打开提交详情 + 评分:%s %s 分钟 diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml index b6bf168689..ee91fbdcd0 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml @@ -426,6 +426,7 @@ 已刪除 + 評分已更新 載入 Canvas 內容… UnknownDevice @@ -1106,6 +1107,7 @@ 您可以從此處開啟提交項目詳細資料 + 評分:%s %s 分鐘 diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index 00d064fb12..013976b360 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -432,6 +432,7 @@ Suprimit + S’ha actualitzat la nota S\'està carregant el contingut de Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Podeu obrir Informació de l’entrega des d\'aquí + Nota: %s %s minut %s minuts diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index 6bd48540a4..8498f8bf6d 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -432,6 +432,7 @@ Wedi dileu + Gradd wedi’i diweddaru Llwytho Cynnwys Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Gallwch chi agor manylion Cyflwyniad fan hyn + Gradd: %s %s Munud %s Munud diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index 5235e2943e..ddc3e5462f 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -432,6 +432,7 @@ Slettet + Karakter opdateret Indlæser Canvas-indhold… UnknownDevice @@ -1119,6 +1120,7 @@ Du kan åbne afleveringsdetaljerne herfra + Karakter: %s %s minut %s minutter diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index 97b9b7ade9..58cda0e850 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -432,6 +432,7 @@ Gelöscht + Note aktualisiert Canvas-Content laden… UnknownDevice @@ -1119,6 +1120,7 @@ Sie können die Abgabedetails von hier aus öffnen + Note: %s %s Minute %s Minuten diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index 94d73a8b46..8aa32c6d13 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -432,6 +432,7 @@ Deleted + Mark updated Loading Canvas Content… UnknownDevice @@ -1119,6 +1120,7 @@ You can open Submission details from here + Mark: %s %s Minute %s Minutes diff --git a/libs/pandares/src/main/res/values-en-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index 4072036e2e..e860a4f857 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -432,6 +432,7 @@ Deleted + Grade updated Loading Canvas Content… UnknownDevice @@ -1119,6 +1120,7 @@ You can open Submission details from here + Grade: %s %s minute %s Minutes diff --git a/libs/pandares/src/main/res/values-en-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index 2b1b2629c7..70d3e97250 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -432,6 +432,7 @@ Deleted + Grade updated Loading Canvas Content… UnknownDevice @@ -1119,6 +1120,7 @@ You can open Submission details from here + Grade: %s %s minute %s Minutes diff --git a/libs/pandares/src/main/res/values-es-rES/strings.xml b/libs/pandares/src/main/res/values-es-rES/strings.xml index e8465b9bf7..8cef4f43c9 100644 --- a/libs/pandares/src/main/res/values-es-rES/strings.xml +++ b/libs/pandares/src/main/res/values-es-rES/strings.xml @@ -432,6 +432,7 @@ Eliminado + Nota actualizada Cargando el contenido de Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Desde aquí puedes abrir los detalles de la entrega + Nota: %s %s minuto %s minutos diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index 22bfecd1b7..1aaa81bfb7 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -432,6 +432,7 @@ Eliminado + Calificación actualizada Cargando el contenido de Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Desde aquí puede abrir los detalles de Entrega + Calificación: %s %s minuto %s minutos diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index d4f361ce9a..ab65479f50 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -432,6 +432,7 @@ Poistettu + Arvosana päivitetty Ladataan Canvas-sisältöä… UnknownDevice @@ -1119,6 +1120,7 @@ Voit avata tehtävän palautustiedot täältä + Arvosana: %s %s Minuutti %s minuuttia diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index 63e5e000cd..dd583f2abc 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -432,6 +432,7 @@ Supprimé + Note mise à jour Chargement du contenu de Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Vous pouvez ouvrir les détails de l’envoi à partir d’ici + Note : %s %s Minute %s Minutes diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index a566c9618a..adaf919f50 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -432,6 +432,7 @@ Supprimé + Note mise à jour Chargement du contenu Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Vous pouvez ouvrir les Détails de soumission à partir d\'ici + Note : %s %s Minute %s Minutes diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index bad97880f0..4d9e1e3de6 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -432,6 +432,7 @@ Efase + Klas aktyalize Chajman Kontni Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Ou ka ouvri detay Soumisyon yo la a + Klas: %s %s Minit %s Minit diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 11335d2433..71498f4b73 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -432,6 +432,7 @@ Eytt + Einkunn uppfærð Sæki Canvas efni… UnknownDevice @@ -1119,6 +1120,7 @@ Þú getur opnað upplýsingar um skil héðan + Einkunn: %s %s Mínúta %s mínútur diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index 95e35d513b..1ebe6ca67a 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -432,6 +432,7 @@ Eliminato + Voto aggiornato Caricamento dei contenuti Contenuto… UnknownDevice @@ -1119,6 +1120,7 @@ Puoi aprire i dettagli consegna da qui + Voto: %s %s minuto %s minuti diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index 44d2184638..0b26bf4991 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -426,6 +426,7 @@ 削除されました + 評定を更新しました Canvas コンテンツ…を読み込み中 UnknownDevice @@ -1106,6 +1107,7 @@ 「提出」の詳細はここで開くことができます + 評定:%s %s 分 diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index 31a742b7fb..3ce51d36f9 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -432,6 +432,7 @@ mukua + Koeke kua whakahoutia E uta ana Canvas Ihirangi… UnknownDevice @@ -1119,6 +1120,7 @@ Mai i konei, ka kite koe i nga Taipitopito Tukunga. + Kōeke: %s %s Meneti %s meneti diff --git a/libs/pandares/src/main/res/values-ms/strings.xml b/libs/pandares/src/main/res/values-ms/strings.xml index 22552f920e..bce19c3d3a 100644 --- a/libs/pandares/src/main/res/values-ms/strings.xml +++ b/libs/pandares/src/main/res/values-ms/strings.xml @@ -432,6 +432,7 @@ Telah Dipadamkan + Gred dikemas kini Memuatkan Kandungan Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Anda boleh membuka butiran Serahan di sini + Gred: %s %s Minit %s Minit diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index fc43bf719c..74ed00f0d0 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -432,6 +432,7 @@ Slettet + Vurdering oppdatert Laster Canvas-innhold… UnknownDevice @@ -1119,6 +1120,7 @@ Du kan åpne Detaljer om innlevering her + Vurdering: %s %s minutt %s minutter diff --git a/libs/pandares/src/main/res/values-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index e5e50a7c83..0b32e79400 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -432,6 +432,7 @@ Verwijderd + Cijfer bijgewerkt Canvas Content aan het uploaden… UnknownDevice @@ -1119,6 +1120,7 @@ U kunt inleverdetails hier openen + Cijfer: %s %s minuut %s minuten diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index f40e84f984..a9f3d91551 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -444,6 +444,7 @@ Usunięto + Zaktualizowano ocenę Ładowanie zawartości Canvas… UnknownDevice @@ -1145,6 +1146,7 @@ Tutaj można otworzyć szczegóły przesyłki + Ocena: %s %s min %s min diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index ce7e963aba..dc5f3aec5c 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -432,6 +432,7 @@ Excluído + Nota atualizada Carregando conteúdo do Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Você pode abrir os detalhes do Envio aqui + Nota: %s %s Minuto %s Minutos diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index 3c73a4d156..1bf5144981 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -432,6 +432,7 @@ Eliminado + Nota atualizada A carregar o conteúdo da tela… UnknownDevice @@ -1119,6 +1120,7 @@ Podes abrir detalhes de Submissão a partir daqui + Nota: %s %s minuto %s minutos diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index 311d8519e6..9c7d3ded27 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -444,6 +444,7 @@ Удалено + Оценка обновлена Загрузка контента Canvas… UnknownDevice @@ -1145,6 +1146,7 @@ Вы можете открыть информацию об отправке здесь + Оценка: %s %s минута %s минут diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index 3dd4497706..b77f4e12fc 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -432,6 +432,7 @@ Odstranjeno + Ocena je posodobljena Nalaganje vsebine sistema Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Tukaj lahko odprete podrobnosti o oddaji + Ocena: %s %s minuta %s minut diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index b923d00664..fc1aee1a9d 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -432,6 +432,7 @@ Borttagen + Omdöme uppdaterat Läser in Canvas-innehåll… UnknownDevice @@ -1119,6 +1120,7 @@ Du kan öppna inlämningsinformationen härifrån + Omdöme: %s %s minut %s minuter diff --git a/libs/pandares/src/main/res/values-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index a0a534be86..e9871f3baf 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -432,6 +432,7 @@ ลบแล้ว + อัพเดตเกรดแล้ว กำลังโหลดเนื้อหา Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ คุณสามารถเปิดรายละเอียดผลงานจัดส่ง (Submission details) ได้จากที่นี่ + เกรด: %s %s นาที %s นาที diff --git a/libs/pandares/src/main/res/values-vi/strings.xml b/libs/pandares/src/main/res/values-vi/strings.xml index db8ee483c2..bcf91a4a53 100644 --- a/libs/pandares/src/main/res/values-vi/strings.xml +++ b/libs/pandares/src/main/res/values-vi/strings.xml @@ -432,6 +432,7 @@ Đã xóa + Đã cập nhật lớp Đang Tải Nội Dung Canvas… UnknownDevice @@ -1119,6 +1120,7 @@ Bạn có thể mở chi tiết Bài Nộp từ đây + Lớp: %s %s Phút %s Phút diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index 815e304d01..9fd0ff9c21 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -426,6 +426,7 @@ 已删除 + 评分已更新 正在加载Canvas内容… UnknownDevice @@ -1106,6 +1107,7 @@ 您可以从此处打开提交详情 + 评分:%s %s 分钟 diff --git a/libs/pandares/src/main/res/values/colors.xml b/libs/pandares/src/main/res/values/colors.xml index 5843539a2c..0adee136e1 100644 --- a/libs/pandares/src/main/res/values/colors.xml +++ b/libs/pandares/src/main/res/values/colors.xml @@ -20,44 +20,44 @@ #556572 #BF32A4 - #EE0612 + #E0061F #556572 #2D3B45 #F5F5F5 #FFFFFF - #008EE2 + #0374B5 #F5F5F5 #FFFFFF #FFFFFF #C7CDD1 - #00AC18 + #0B874B #FC5E13 #BF32A4 #BF32A4 - #EE0612 + #E0061F #556572 #2D3B45 - #008EE2 + #0374B5 #F5F5F5 #FFFFFF #C7CDD1 - #00AC18 + #0B874B #FC5E13 - #EE0612 - #008EE2 + #E0061F + #0374B5 #FC5E13 #2D3B45 #394B58 #F5F5F5 - #00AC18 + #0B874B #BF32A4 - #EE0612 + #E0061F #556572 #2D3B45 - #008EE2 + #0374B5 #F5F5F5 #FFFFFF - #00AC18 + #0B874B #FC5E13 #C7CDD1 #FFFFFF diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index c1648b020a..01f7f5f4cd 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1400,4 +1400,5 @@ Email Version There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo diff --git a/libs/pandautils/flank.yml b/libs/pandautils/flank.yml index 48bcce337e..60bd72123a 100644 --- a/libs/pandautils/flank.yml +++ b/libs/pandautils/flank.yml @@ -10,8 +10,8 @@ gcloud: test-targets: - notAnnotation com.instructure.canvas.espresso.E2E, com.instructure.canvas.espresso.Stub, com.instructure.canvas.espresso.FlakyE2E, com.instructure.canvas.espresso.KnownBug device: - - model: Nexus6P - version: 26 + - model: Pixel2.arm + version: 29 locale: en_US orientation: portrait diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/RatingDialog.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/RatingDialog.kt index 04cf4f703d..7745073010 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/RatingDialog.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/dialogs/RatingDialog.kt @@ -113,13 +113,11 @@ class RatingDialog : DialogFragment() { val starClickListener = View.OnClickListener { v -> stars.forEach { - it.setImageResource(R.drawable.ic_star) - it.setColorFilter(requireContext().getColor(R.color.backgroundMedium)) + it.setImageResource(R.drawable.ic_rating_star_outline) } val selectionIndex = stars.indexOf(v) stars.take(selectionIndex + 1).forEach { - it.setImageResource(R.drawable.ic_star) - it.setColorFilter(requireContext().getColor(R.color.backgroundDark)) + it.setImageResource(R.drawable.ic_rating_star) } val isFiveStars = selectionIndex >= 4 comments.setVisible(!isFiveStars) @@ -132,7 +130,6 @@ class RatingDialog : DialogFragment() { } stars.forEach { - it.setImageResource(R.drawable.ic_star) it.setOnClickListener(starClickListener) } } diff --git a/libs/pandautils/src/main/res/color/calendar_color_selector.xml b/libs/pandautils/src/main/res/color/calendar_color_selector.xml new file mode 100644 index 0000000000..1ba3935ed9 --- /dev/null +++ b/libs/pandautils/src/main/res/color/calendar_color_selector.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/layout/dialog_rating.xml b/libs/pandautils/src/main/res/layout/dialog_rating.xml index 007ed070e7..ab56eb6e20 100644 --- a/libs/pandautils/src/main/res/layout/dialog_rating.xml +++ b/libs/pandautils/src/main/res/layout/dialog_rating.xml @@ -39,8 +39,8 @@ android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:contentDescription="@string/star1" - android:src="@drawable/ic_star" - app:tint="@color/backgroundMedium" /> + android:src="@drawable/ic_rating_star_outline" + app:tint="@color/backgroundDark" /> + android:src="@drawable/ic_rating_star_outline" + app:tint="@color/backgroundDark"/> + android:src="@drawable/ic_rating_star_outline" + app:tint="@color/backgroundDark"/> + android:src="@drawable/ic_rating_star_outline" + app:tint="@color/backgroundDark"/> + android:src="@drawable/ic_rating_star_outline" + app:tint="@color/backgroundDark"/> diff --git a/libs/pandautils/src/main/res/layout/fragment_about.xml b/libs/pandautils/src/main/res/layout/fragment_about.xml index 5d26338930..12b2463385 100644 --- a/libs/pandautils/src/main/res/layout/fragment_about.xml +++ b/libs/pandautils/src/main/res/layout/fragment_about.xml @@ -139,7 +139,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" - android:importantForAccessibility="no" + android:contentDescription="@string/instructure_logo" android:src="@drawable/ic_instructure_logo" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/libs/pandautils/src/main/res/values/font_styles.xml b/libs/pandautils/src/main/res/values/font_styles.xml index 1e19ec8b65..51fa9e391c 100644 --- a/libs/pandautils/src/main/res/values/font_styles.xml +++ b/libs/pandautils/src/main/res/values/font_styles.xml @@ -42,4 +42,8 @@ italic + + \ No newline at end of file diff --git a/libs/pandautils/src/main/res/values/styles.xml b/libs/pandautils/src/main/res/values/styles.xml index 87edd80e33..ea6efca1aa 100644 --- a/libs/pandautils/src/main/res/values/styles.xml +++ b/libs/pandautils/src/main/res/values/styles.xml @@ -219,4 +219,8 @@ @color/white + +