diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 39139b8c4f..5aaaa758ca 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -57,8 +57,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 225 - versionName = '6.10.0' + versionCode = 226 + versionName = '6.11.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true 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 c62e9f6380..001b04e780 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 @@ -49,6 +49,26 @@ class AssignmentListInteractionTest : StudentTest() { assignmentListPage.assertHasAssignment(assignment) } + @Test + @TestMetaData(Priority.P1, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun sortAssignmentsByTimeByDefault() { + val assignment = getToAssignmentsPage()[0] + assignmentListPage.assertHasAssignment(assignment) + assignmentListPage.assertSortByButtonShowsSortByTime() + assignmentListPage.assertFindsUndatedAssignmentLabel() + } + + @Test + @TestMetaData(Priority.P1, FeatureCategory.ASSIGNMENTS, TestCategory.INTERACTION) + fun sortAssignmentsByTypeWhenTypeIsSelectedInTheDialog() { + val assignment = getToAssignmentsPage()[0] + + assignmentListPage.selectSortByType() + + assignmentListPage.assertHasAssignment(assignment) + assignmentListPage.assertSortByButtonShowsSortByType() + } + private fun getToAssignmentsPage(assignmentCount: Int = 1): List { val data = MockCanvas.init( courseCount = 1, 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 17b8731f8c..405413750c 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 @@ -19,25 +19,16 @@ package com.instructure.student.ui.pages import android.view.View import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withChild -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withParent -import androidx.test.espresso.matcher.ViewMatchers.withText -import com.instructure.canvasapi2.models.Assignment +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.waitForMatcherWithRefreshes +import com.instructure.canvasapi2.models.Assignment import com.instructure.dataseeding.model.AssignmentApiModel import com.instructure.dataseeding.model.QuizApiModel -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.WaitForViewWithText -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.click +import com.instructure.espresso.* import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.waitForViewWithText -import com.instructure.espresso.scrollTo -import com.instructure.espresso.swipeDown import com.instructure.student.R import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf @@ -46,13 +37,13 @@ import org.hamcrest.Matchers.containsString class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) { private val assignmentListToolbar by OnViewWithId(R.id.toolbar) + private val gradingPeriodHeader by WaitForViewWithId(R.id.termSpinnerLayout) + private val sortByButton by OnViewWithId(R.id.sortByButton) + private val sortByTextView by OnViewWithId(R.id.sortByTextView) // Only displayed when assignment list is empty private val emptyView by WaitForViewWithId(R.id.emptyView, autoAssert = false) - // Only displayed when there are grading periods - private val gradingPeriodHeader by WaitForViewWithId(R.id.termSpinnerLayout, autoAssert = false) - // Only displayed when there are no assignments private val emptyText by WaitForViewWithText(R.string.noItemsToDisplayShort, autoAssert = false) @@ -140,4 +131,21 @@ class AssignmentListPage : BasePage(pageResId = R.id.assignmentListPage) { fun assertHasGradingPeriods() { gradingPeriodHeader.assertDisplayed() } + + fun assertSortByButtonShowsSortByTime() { + sortByTextView.check(matches(withText(R.string.sortByTime))) + } + + fun assertSortByButtonShowsSortByType() { + sortByTextView.check(matches(withText(R.string.sortByType))) + } + + fun assertFindsUndatedAssignmentLabel() { + onView(withText(R.string.undatedAssignments)).assertVisible() + } + + fun selectSortByType() { + sortByButton.click() + onView(withText(R.string.sortByDialogTypeOption)).click() + } } 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 ba23e60e0f..08cc945468 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 @@ -55,49 +55,49 @@ abstract class StudentTest : CanvasTest() { /** * Required for auto complete of page objects within tests */ + val allCoursesPage = AllCoursesPage() + val annotationCommentListPage = AnnotationCommentListPage() + val assignmentDetailsPage = AssignmentDetailsPage() val assignmentListPage = AssignmentListPage() + val bookmarkPage = BookmarkPage() + val calendarEventPage = CalendarEventPage() + val calendarPage = CalendarPage() + val canvasWebViewPage = CanvasWebViewPage() + val courseBrowserPage = CourseBrowserPage() + val courseGradesPage = CourseGradesPage() val dashboardPage = DashboardPage() - val allCoursesPage = AllCoursesPage() + val discussionDetailsPage = DiscussionDetailsPage() + val discussionListPage = DiscussionListPage() val editFavoritesPage = EditFavoritesPage() - val calendarPage = CalendarPage() - val todoPage = TodoPage() - val inboxPage = InboxPage() + val fileListPage = FileListPage() + val fileUploadPage = FileUploadPage() + val helpPage = HelpPage() val inboxConversationPage = InboxConversationPage() - val newMessagePage = NewMessagePage() - val settingsPage = SettingsPage() - val pairObserverPage = PairObserverPage() + val inboxPage = InboxPage() val legalPage = LegalPage() - val helpPage = HelpPage() val loginFindSchoolPage = LoginFindSchoolPage() val loginLandingPage = LoginLandingPage() val loginSignInPage = LoginSignInPage() - val qrLoginPage = QRLoginPage() - val courseBrowserPage = CourseBrowserPage() - val assignmentDetailsPage = AssignmentDetailsPage() - val submissionDetailsPage = SubmissionDetailsPage() - val peopleListPage = PeopleListPage() - val personDetailsPage = PersonDetailsPage() + val moduleProgressionPage = ModuleProgressionPage() val modulesPage = ModulesPage() - val syllabusPage = SyllabusPage() - val fileListPage = FileListPage() - val discussionListPage = DiscussionListPage() - val discussionDetailsPage = DiscussionDetailsPage() + val newMessagePage = NewMessagePage() + val notificationPage = NotificationPage() val pageListPage = PageListPage() - val quizListPage = QuizListPage() - val urlSubmissionUploadPage = UrlSubmissionUploadPage() - val courseGradesPage = CourseGradesPage() - val moduleProgressionPage = ModuleProgressionPage() - val canvasWebViewPage = CanvasWebViewPage() - val fileUploadPage = FileUploadPage() - val annotationCommentListPage = AnnotationCommentListPage() + val pairObserverPage = PairObserverPage() + val pandaAvatarPage = PandaAvatarPage() + val peopleListPage = PeopleListPage() + val personDetailsPage = PersonDetailsPage() val pickerSubmissionUploadPage = PickerSubmissionUploadPage() - val remoteConfigSettingsPage = RemoteConfigSettingsPage() val profileSettingsPage = ProfileSettingsPage() - val calendarEventPage = CalendarEventPage() + val qrLoginPage = QRLoginPage() + val quizListPage = QuizListPage() val quizTakingPage = QuizTakingPage() - val pandaAvatarPage = PandaAvatarPage() - val notificationPage = NotificationPage() - val bookmarkPage = BookmarkPage() + val remoteConfigSettingsPage = RemoteConfigSettingsPage() + val settingsPage = SettingsPage() + val submissionDetailsPage = SubmissionDetailsPage() + val syllabusPage = SyllabusPage() + val todoPage = TodoPage() + val urlSubmissionUploadPage = UrlSubmissionUploadPage() // A no-op interaction to afford us an easy, harmless way to get a11y checking to trigger. fun meaninglessSwipe() { diff --git a/apps/student/src/main/AndroidManifest.xml b/apps/student/src/main/AndroidManifest.xml index dcacead26f..9c2f96dc18 100644 --- a/apps/student/src/main/AndroidManifest.xml +++ b/apps/student/src/main/AndroidManifest.xml @@ -63,7 +63,6 @@ android:hardwareAccelerated="true" android:supportsRtl="true" android:largeHeap="true" - android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" android:networkSecurityConfig="@xml/network_security_config" tools:replace="android:supportsRtl" diff --git a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt index bd4cfb5ea6..5c6c6efc8e 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/ShareFileUploadActivity.kt @@ -39,6 +39,7 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.StorageQuotaExceededError import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave @@ -131,6 +132,7 @@ class ShareFileUploadActivity : AppCompatActivity(), ShareFileDestinationDialog. private fun getCourses() { loadCoursesJob = tryWeave { val courses = awaitApi> { CourseManager.getCourses(true, it) } + .filter { it.isNotDeleted() } if (courses.isNotEmpty()) { this@ShareFileUploadActivity.courses = ArrayList(courses) if (uploadFileSourceFragment == null) showDestinationDialog() diff --git a/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt index e8dd7b4325..e36ac0a836 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/AllCoursesRecyclerAdapter.kt @@ -20,17 +20,18 @@ package com.instructure.student.adapter import android.app.Activity import android.view.View import android.widget.Toast -import com.instructure.student.R -import com.instructure.student.holders.CourseViewHolder -import com.instructure.student.interfaces.CourseAdapterToFragmentCallback import com.instructure.canvasapi2.managers.CourseManager import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.APIHelper import com.instructure.canvasapi2.utils.isInvited +import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.weave.WeaveJob import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave +import com.instructure.student.R +import com.instructure.student.holders.CourseViewHolder +import com.instructure.student.interfaces.CourseAdapterToFragmentCallback class AllCoursesRecyclerAdapter( @@ -57,7 +58,7 @@ class AllCoursesRecyclerAdapter( mApiCall?.cancel() mApiCall = tryWeave { val courses = awaitApi> { CourseManager.getCourses(isRefresh, it) } - .filter { !it.accessRestrictedByDate && !it.isInvited() } + .filter { !it.accessRestrictedByDate && !it.isInvited() && it.isNotDeleted() } addAll(courses) notifyDataSetChanged() isAllPagesLoaded = true diff --git a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt index 70e662359b..ecb1220cd5 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/DashboardRecyclerAdapter.kt @@ -161,7 +161,7 @@ class DashboardRecyclerAdapter( // Get enrollment invites val invites = awaitApi> { - EnrollmentManager.getSelfEnrollments(null, listOf(EnrollmentAPI.STATE_INVITED), isRefresh, it) + EnrollmentManager.getSelfEnrollments(null, listOf(EnrollmentAPI.STATE_INVITED, EnrollmentAPI.STATE_CURRENT_AND_FUTURE), isRefresh, it) } // Map not null is needed because the dashboard api can return unpublished courses @@ -205,10 +205,7 @@ class DashboardRecyclerAdapter( addOrUpdateAllItems(ItemType.ANNOUNCEMENT_HEADER, announcements) // Add course invites - val validInvites = invites.filter { - mCourseMap[it.courseId]?.let { course -> - course.isValidTerm() && !course.accessRestrictedByDate && isEnrollmentBetweenCourseDatesOrNotRestricted(course) } ?: false - } + val validInvites = invites.filter { it.enrollmentState == EnrollmentAPI.STATE_INVITED && hasValidCourseForEnrollment(it) } addOrUpdateAllItems(ItemType.INVITATION_HEADER, validInvites) @@ -222,13 +219,20 @@ class DashboardRecyclerAdapter( } } - private fun isEnrollmentBetweenCourseDatesOrNotRestricted(course: Course): Boolean { - val now = OffsetDateTime.now() - val startDate = OffsetDateTime.parse(course.startAt).withOffsetSameInstant(OffsetDateTime.now().offset) - val endDate = OffsetDateTime.parse(course.endAt).withOffsetSameInstant(OffsetDateTime.now().offset) + private fun hasValidCourseForEnrollment(enrollment: Enrollment): Boolean { + return mCourseMap[enrollment.courseId]?.let { course -> + course.isValidTerm() && !course.accessRestrictedByDate && isEnrollmentBeforeEndDateOrNotRestricted(course) + } ?: false + } + + private fun isEnrollmentBeforeEndDateOrNotRestricted(course: Course): Boolean { + val isBeforeEndDate = course.endAt?.let { + val now = OffsetDateTime.now() + val endDate = OffsetDateTime.parse(it).withOffsetSameInstant(OffsetDateTime.now().offset) + now.isBefore(endDate) + } ?: true // Case when the course has no end date - val isBetweenCourseDates = now.isAfter(startDate) && now.isBefore(endDate) - return !course.restrictEnrollmentsToCourseDate || isBetweenCourseDates + return !course.restrictEnrollmentsToCourseDate || isBeforeEndDate } override fun itemLayoutResId(viewType: Int) = when (ItemType.values()[viewType]) { diff --git a/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt index ba433dbb96..392ff643bc 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/EditFavoritesRecyclerAdapter.kt @@ -25,7 +25,7 @@ import com.instructure.canvasapi2.models.CanvasComparable import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.isInvited -import com.instructure.canvasapi2.utils.isValidTerm +import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.weave.WeaveJob import com.instructure.canvasapi2.utils.weave.awaitApis import com.instructure.canvasapi2.utils.weave.catch @@ -129,7 +129,7 @@ class EditFavoritesRecyclerAdapter( val (rawCourses, rawGroups) = awaitApis,List>( { CourseManager.getCourses(true, it) }, { GroupManager.getAllGroups(it,true)}) - val validCourses = rawCourses.filter { !it.accessRestrictedByDate && !it.isInvited() } + val validCourses = rawCourses.filter { !it.accessRestrictedByDate && !it.isInvited() && it.isNotDeleted() } addOrUpdateAllItems(ItemType.COURSE_HEADER,validCourses) val courseMap = rawCourses.associateBy { it.id } val groups = rawGroups.filter { group -> group.isActive(courseMap[group.courseId]) } diff --git a/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt index cba6858fda..66d75e01b0 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/ModuleListRecyclerAdapter.kt @@ -57,9 +57,10 @@ import retrofit2.Response import java.util.* open class ModuleListRecyclerAdapter( - val courseContext: CanvasContext, + private val courseContext: CanvasContext, context: Context, - val adapterToFragmentCallback: ModuleAdapterToFragmentCallback? + private var shouldExhaustPagination: Boolean, + private val adapterToFragmentCallback: ModuleAdapterToFragmentCallback? ) : ExpandableRecyclerAdapter(context, ModuleObject::class.java, ModuleItem::class.java) { private val mModuleItemCallbacks = HashMap() @@ -67,7 +68,7 @@ open class ModuleListRecyclerAdapter( private var checkCourseTabsJob: Job? = null /* For testing purposes only */ - protected constructor(context: Context) : this(CanvasContext.defaultCanvasContext(), context, null) // Callback not needed for testing, cast to null + protected constructor(context: Context) : this(CanvasContext.defaultCanvasContext(), context, false,null) // Callback not needed for testing, cast to null init { viewHolderHeaderClicked = object : ViewHolderHeaderClicked { @@ -83,7 +84,7 @@ open class ModuleListRecyclerAdapter( } isExpandedByDefault = false -// isDisplayEmptyCell = true TODO - make this work with scroll to functionality + isDisplayEmptyCell = true if (adapterToFragmentCallback != null) loadData() // Callback is null when testing } @@ -136,6 +137,7 @@ open class ModuleListRecyclerAdapter( } override fun refresh() { + shouldExhaustPagination = false mModuleItemCallbacks.clear() checkCourseTabsJob?.cancel() collapseAll() @@ -339,8 +341,8 @@ open class ModuleListRecyclerAdapter( ModuleManager.getFirstPageModuleItems(courseContext, it.id, getModuleItemsCallback(it, true), true) } } - if(!this.moreCallsExist()) { - // Wait until we are done exhausting pagination + if(!shouldExhaustPagination || !this.moreCallsExist()) { + // If we should exhaust pagination wait until we are done exhausting pagination adapterToFragmentCallback?.onRefreshFinished() } } @@ -358,7 +360,11 @@ open class ModuleListRecyclerAdapter( // We only want to show modules if its a course nav option OR set to as the homepage if (tabs.find { it.tabId == "modules" } != null || (courseContext as Course).homePage?.apiString == "modules") { - ModuleManager.getAllModuleObjets(courseContext, mModuleObjectCallback!!, true) + if (shouldExhaustPagination) { + ModuleManager.getAllModuleObjets(courseContext, mModuleObjectCallback!!, true) + } else { + ModuleManager.getFirstPageModuleObjects(courseContext, mModuleObjectCallback!!, true) + } } else { adapterToFragmentCallback?.onRefreshFinished(true) } @@ -367,6 +373,10 @@ open class ModuleListRecyclerAdapter( } } + override fun loadNextPage(nextURL: String) { + ModuleManager.getNextPageModuleObjects(nextURL, mModuleObjectCallback!!, true) + } + // endregion // region Module binder Helpers 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 a57e35d8c8..5543220bf3 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 @@ -34,6 +34,7 @@ import com.instructure.canvasapi2.utils.ApiPrefs.user import com.instructure.canvasapi2.utils.ApiType import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.LinkHeaders +import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.pandarecycler.util.GroupSortedList.GroupComparatorCallback import com.instructure.pandarecycler.util.GroupSortedList.ItemComparatorCallback import com.instructure.pandarecycler.util.Types @@ -156,7 +157,8 @@ class NotificationListRecyclerAdapter( coursesCallback = object : StatusCallback>() { override fun onResponse(response: Response>, linkHeaders: LinkHeaders, type: ApiType) { - courseMap = createCourseMap(response.body()) + val courses = response.body()?.filter { it.isNotDeleted() } + courseMap = createCourseMap(courses) populateActivityStreamAdapter() } } diff --git a/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt index 75cca95596..638036d152 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/TermSpinnerAdapter.kt @@ -31,7 +31,8 @@ import com.instructure.student.R class TermSpinnerAdapter( context: Context, resource: Int, - private val gradingPeriods: List + private val gradingPeriods: List, + private val showDropdownArrow: Boolean = true ) : ArrayAdapter(context, resource, gradingPeriods) { private val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater @@ -54,7 +55,7 @@ class TermSpinnerAdapter( holder = view.tag as TermSpinnerViewHolder } - holder.dropDown.setVisible(!isLoading) + holder.dropDown.setVisible(!isLoading && showDropdownArrow) holder.progressBar.setVisible(isLoading) holder.periodName.text = gradingPeriods[position].title diff --git a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt index b50b7c972b..5c19c1368e 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/TodoListRecyclerAdapter.kt @@ -223,7 +223,7 @@ open class TodoListRecyclerAdapter : ExpandableRecyclerAdapter>, linkHeaders: LinkHeaders, type: ApiType) { val body = response.body() ?: return val filteredCourses = body.filter { - !it.accessRestrictedByDate && !it.isInvited() && (when (filterMode) { + !it.accessRestrictedByDate && !it.isInvited() && it.isNotDeleted() && (when (filterMode) { is FavoritedCourses -> it.isFavorite else -> true }) diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt new file mode 100644 index 0000000000..77585e5dc5 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByDateRecyclerAdapter.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.adapter.assignment + +import android.content.Context +import com.instructure.canvasapi2.models.* +import com.instructure.canvasapi2.utils.* +import com.instructure.pandarecycler.util.GroupSortedList +import com.instructure.pandarecycler.util.Types +import com.instructure.student.R +import com.instructure.student.interfaces.AdapterToAssignmentsCallback +import java.util.* + +private const val HEADER_POSITION_OVERDUE = 0 +private const val HEADER_POSITION_UPCOMING = 1 +private const val HEADER_POSITION_UNDATED = 2 +private const val HEADER_POSITION_PAST = 3 + +class AssignmentListByDateRecyclerAdapter( + context: Context, + canvasContext: CanvasContext, + adapterToAssignmentsCallback: AdapterToAssignmentsCallback, + isTesting: Boolean = false +) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting) { + + private val overdue = AssignmentGroup(name = context.getString(R.string.overdueAssignments), position = HEADER_POSITION_OVERDUE) + private val upcoming = AssignmentGroup(name = context.getString(R.string.upcomingAssignments), position = HEADER_POSITION_UPCOMING) + private val undated = AssignmentGroup(name = context.getString(R.string.undatedAssignments), position = HEADER_POSITION_UNDATED) + private val past = AssignmentGroup(name = context.getString(R.string.pastAssignments), position = HEADER_POSITION_PAST) + + override fun createItemCallback() = object : GroupSortedList.ItemComparatorCallback { + private val sameCheck = compareBy({ it.dueAt }, { it.name }) + override fun areContentsTheSame(old: Assignment, new: Assignment) = sameCheck.compare(old, new) == 0 + override fun areItemsTheSame(item1: Assignment, item2: Assignment) = item1.id == item2.id + override fun getChildType(group: AssignmentGroup, item: Assignment) = Types.TYPE_ITEM + override fun getUniqueItemId(item: Assignment) = item.id + override fun compare(group: AssignmentGroup, o1: Assignment, o2: Assignment): Int { + return when (group.position) { + HEADER_POSITION_UNDATED -> o1.name?.toLowerCase()?.compareTo(o2.name?.toLowerCase() ?: "") ?: 0 + HEADER_POSITION_PAST -> o2.dueAt?.compareTo(o1.dueAt ?: "") ?: 0 // Sort newest date first (o1 and o2 switched places) + else -> o1.dueAt?.compareTo(o2.dueAt ?: "") ?: 0 // Sort oldest date first + } + } + } + + override fun populateData() { + val today = Date() + for (assignmentGroup in assignmentGroups) { + // TODO canHaveOverDueAssignment + // web does it like this + // # only handles observer observing one student, this needs to change to handle multiple users in the future + // canHaveOverdueAssignment = !ENV.current_user_has_been_observer_in_this_course || ENV.observed_student_ids?.length == 1I + // endtodo + assignmentGroup.assignments + .filterWithQuery(searchQuery, Assignment::name) + .forEach { assignment -> + val dueAt = assignment.dueAt + val submission = assignment.submission + assignment.submission = submission + val isWithoutGradedSubmission = submission == null || submission.isWithoutGradedSubmission + val isOverdue = assignment.isAllowedToSubmit && isWithoutGradedSubmission + if (dueAt == null) { + addOrUpdateItem(undated, assignment) + } else { + when { + today.before(dueAt.toDate()) -> addOrUpdateItem(upcoming, assignment) + isOverdue -> addOrUpdateItem(overdue, assignment) + else -> addOrUpdateItem(past, assignment) + } + } + } + } + isAllPagesLoaded = true + } + +} diff --git a/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt new file mode 100644 index 0000000000..569115e6c1 --- /dev/null +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListByTypeRecyclerAdapter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.student.adapter.assignment + +import android.content.Context +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.AssignmentGroup +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.utils.filterWithQuery +import com.instructure.pandarecycler.util.GroupSortedList +import com.instructure.pandarecycler.util.Types +import com.instructure.student.interfaces.AdapterToAssignmentsCallback + +class AssignmentListByTypeRecyclerAdapter( + context: Context, + canvasContext: CanvasContext, + adapterToAssignmentsCallback: AdapterToAssignmentsCallback, + isTesting: Boolean = false +) : AssignmentListRecyclerAdapter(context, canvasContext, adapterToAssignmentsCallback, isTesting) { + + override fun populateData() { + assignmentGroups.forEach { assignmentGroup -> + val filteredAssignments = assignmentGroup.assignments.filterWithQuery(searchQuery, Assignment::name) + addOrUpdateAllItems(assignmentGroup, filteredAssignments) + } + isAllPagesLoaded = true + } + + override fun createItemCallback() = object : GroupSortedList.ItemComparatorCallback { + private val sameCheck = compareBy({ it.dueAt }, { it.name }) + override fun areContentsTheSame(old: Assignment, new: Assignment) = sameCheck.compare(old, new) == 0 + override fun areItemsTheSame(item1: Assignment, item2: Assignment) = item1.id == item2.id + override fun getChildType(group: AssignmentGroup, item: Assignment) = Types.TYPE_ITEM + override fun getUniqueItemId(item: Assignment) = item.id + override fun compare(group: AssignmentGroup, o1: Assignment, o2: Assignment) = o1.position - o2.position + } +} diff --git a/apps/student/src/main/java/com/instructure/student/adapter/AssignmentDateListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt similarity index 61% rename from apps/student/src/main/java/com/instructure/student/adapter/AssignmentDateListRecyclerAdapter.kt rename to apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt index edce374397..57620483b9 100644 --- a/apps/student/src/main/java/com/instructure/student/adapter/AssignmentDateListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/adapter/assignment/AssignmentListRecyclerAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 - present Instructure, Inc. + * Copyright (C) 2021 - present Instructure, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,8 +14,7 @@ * along with this program. If not, see . * */ - -package com.instructure.student.adapter +package com.instructure.student.adapter.assignment import android.content.Context import android.view.View @@ -33,6 +32,7 @@ import com.instructure.pandarecycler.util.GroupSortedList import com.instructure.pandarecycler.util.Types import com.instructure.pandautils.utils.color import com.instructure.student.R +import com.instructure.student.adapter.ExpandableRecyclerAdapter import com.instructure.student.holders.AssignmentViewHolder import com.instructure.student.holders.EmptyViewHolder import com.instructure.student.holders.ExpandableViewHolder @@ -40,27 +40,22 @@ import com.instructure.student.interfaces.AdapterToAssignmentsCallback import com.instructure.student.interfaces.GradingPeriodsCallback import retrofit2.Call import retrofit2.Response -import java.util.* -open class AssignmentDateListRecyclerAdapter( - context: Context, - private val canvasContext: CanvasContext, - private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback, - isTesting: Boolean = false +abstract class AssignmentListRecyclerAdapter ( + context: Context, + private val canvasContext: CanvasContext, + private val adapterToAssignmentsCallback: AdapterToAssignmentsCallback, + isTesting: Boolean = false ) : ExpandableRecyclerAdapter( - context, - AssignmentGroup::class.java, - Assignment::class.java + context, + AssignmentGroup::class.java, + Assignment::class.java ), GradingPeriodsCallback { - private val overdue: AssignmentGroup - private val upcoming: AssignmentGroup - private val undated: AssignmentGroup - private val past: AssignmentGroup private var assignmentGroupCallback: StatusCallback>? = null override var currentGradingPeriod: GradingPeriod? = null private var apiJob: WeaveJob? = null - private var assignmentGroups: List = emptyList() + protected var assignmentGroups: List = emptyList() var searchQuery: String = "" set(value) { @@ -73,10 +68,6 @@ open class AssignmentDateListRecyclerAdapter( } init { - overdue = AssignmentGroup(name = context.getString(R.string.overdueAssignments), position = HEADER_POSITION_OVERDUE) - upcoming = AssignmentGroup(name = context.getString(R.string.upcomingAssignments), position = HEADER_POSITION_UPCOMING) - undated = AssignmentGroup(name = context.getString(R.string.undatedAssignments), position = HEADER_POSITION_UNDATED) - past = AssignmentGroup(name = context.getString(R.string.pastAssignments), position = HEADER_POSITION_PAST) isExpandedByDefault = true isDisplayEmptyCell = true if (!isTesting) loadData() @@ -89,15 +80,15 @@ open class AssignmentDateListRecyclerAdapter( assignmentGroups = response.body()!! populateData() adapterToAssignmentsCallback.onRefreshFinished() - adapterToAssignmentsCallback.setTermSpinnerState(true) + adapterToAssignmentsCallback.assignmentLoadingFinished() } override fun onFail(call: Call>?, error: Throwable, response: Response<*>?) { - adapterToAssignmentsCallback.setTermSpinnerState(true) + adapterToAssignmentsCallback.assignmentLoadingFinished() } override fun onFinished(type: ApiType) { - this@AssignmentDateListRecyclerAdapter.onCallbackFinished(type) + this@AssignmentListRecyclerAdapter.onCallbackFinished(type) } } @@ -127,7 +118,7 @@ open class AssignmentDateListRecyclerAdapter( //This check is for the "all grading periods" option if (currentGradingPeriod != null && currentGradingPeriod!!.title != null - && currentGradingPeriod!!.title == context.getString(R.string.allGradingPeriods)) { + && currentGradingPeriod!!.title == context.getString(R.string.assignmentsListAllGradingPeriods)) { loadAssignment() return } @@ -150,6 +141,8 @@ open class AssignmentDateListRecyclerAdapter( loadAssignmentsForGradingPeriod(currentGradingPeriod!!.id, true) return } + } else { + adapterToAssignmentsCallback.gradingPeriodsFetched(emptyList()) } } //If we made it this far, MGP is disabled so we just go forward with the standard @@ -163,6 +156,7 @@ open class AssignmentDateListRecyclerAdapter( }.gradingPeriodList adapterToAssignmentsCallback.gradingPeriodsFetched(periods) } catch { + adapterToAssignmentsCallback.gradingPeriodsFetched(emptyList()) Logger.w("Unable to fetch grading periods") it.printStackTrace() } @@ -173,9 +167,9 @@ open class AssignmentDateListRecyclerAdapter( override val isPaginated get() = false override fun onBindChildHolder( - holder: RecyclerView.ViewHolder, - assignmentGroup: AssignmentGroup, - assignment: Assignment + holder: RecyclerView.ViewHolder, + assignmentGroup: AssignmentGroup, + assignment: Assignment ) { (holder as AssignmentViewHolder).bind(context, assignment, canvasContext.color, adapterToAssignmentsCallback) } @@ -185,16 +179,16 @@ open class AssignmentDateListRecyclerAdapter( } override fun onBindHeaderHolder( - holder: RecyclerView.ViewHolder, - assignmentGroup: AssignmentGroup, - isExpanded: Boolean + holder: RecyclerView.ViewHolder, + assignmentGroup: AssignmentGroup, + isExpanded: Boolean ) { (holder as ExpandableViewHolder).bind( - context, - assignmentGroup, - assignmentGroup.name ?: "", - isExpanded, - viewHolderHeaderClicked + context, + assignmentGroup, + assignmentGroup.name ?: "", + isExpanded, + viewHolderHeaderClicked ) } @@ -206,11 +200,11 @@ open class AssignmentDateListRecyclerAdapter( // Scope assignments if its for a student val scopeToStudent = (canvasContext as Course).isStudent AssignmentManager.getAssignmentGroupsWithAssignmentsForGradingPeriod( - canvasContext.id, - gradingPeriodID, - scopeToStudent, - isRefresh, - assignmentGroupCallback!! + canvasContext.id, + gradingPeriodID, + scopeToStudent, + isRefresh, + assignmentGroupCallback!! ) } @@ -218,35 +212,7 @@ open class AssignmentDateListRecyclerAdapter( AssignmentManager.getAssignmentGroupsWithAssignments(canvasContext.id, isRefresh, assignmentGroupCallback!!) } - private fun populateData() { - val today = Date() - for (assignmentGroup in assignmentGroups) { - // TODO canHaveOverDueAssignment - // web does it like this - // # only handles observer observing one student, this needs to change to handle multiple users in the future - // canHaveOverdueAssignment = !ENV.current_user_has_been_observer_in_this_course || ENV.observed_student_ids?.length == 1I - // endtodo - assignmentGroup.assignments - .filterWithQuery(searchQuery, Assignment::name) - .forEach { assignment -> - val dueAt = assignment.dueAt - val submission = assignment.submission - assignment.submission = submission - val isWithoutGradedSubmission = submission == null || submission.isWithoutGradedSubmission - val isOverdue = assignment.isAllowedToSubmit && isWithoutGradedSubmission - if (dueAt == null) { - addOrUpdateItem(undated, assignment) - } else { - when { - today.before(dueAt.toDate()) -> addOrUpdateItem(upcoming, assignment) - isOverdue -> addOrUpdateItem(overdue, assignment) - else -> addOrUpdateItem(past, assignment) - } - } - } - } - isAllPagesLoaded = true - } + protected abstract fun populateData() // region Expandable callbacks @@ -258,37 +224,10 @@ open class AssignmentDateListRecyclerAdapter( override fun getUniqueGroupId(group: AssignmentGroup) = group.position.toLong() } - override fun createItemCallback() = itemCallback - // endregion override fun cancel() { super.cancel() apiJob?.cancel() } - - companion object { - const val HEADER_POSITION_OVERDUE = 0 - const val HEADER_POSITION_UPCOMING = 1 - const val HEADER_POSITION_UNDATED = 2 - const val HEADER_POSITION_PAST = 3 - - // Decoupled for testability - val itemCallback = object : GroupSortedList.ItemComparatorCallback { - private val sameCheck = compareBy({ it.dueAt }, { it.name }) - override fun areContentsTheSame(old: Assignment, new: Assignment) = sameCheck.compare(old, new) == 0 - override fun areItemsTheSame(item1: Assignment, item2: Assignment) = item1.id == item2.id - override fun getChildType(group: AssignmentGroup, item: Assignment) = Types.TYPE_ITEM - override fun getUniqueItemId(item: Assignment) = item.id - override fun compare(group: AssignmentGroup, o1: Assignment, o2: Assignment): Int { - val position = group.position - return when (position) { - HEADER_POSITION_UNDATED -> o1.name?.toLowerCase()?.compareTo(o2.name?.toLowerCase() ?: "") ?: 0 - HEADER_POSITION_PAST -> o2.dueAt?.compareTo(o1.dueAt ?: "") ?: 0 // Sort newest date first (o1 and o2 switched places) - else -> o1.dueAt?.compareTo(o2.dueAt ?: "") ?: 0 // Sort oldest date first - } - } - } - } - -} +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt b/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt index 6ba6e9e6cf..e5f3d26215 100644 --- a/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt +++ b/apps/student/src/main/java/com/instructure/student/dialog/CanvasContextListDialog.kt @@ -30,6 +30,7 @@ import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.hasActiveEnrollment +import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.isValidTerm import com.instructure.canvasapi2.utils.weave.awaitApis import com.instructure.canvasapi2.utils.weave.catch @@ -94,7 +95,7 @@ class CanvasContextListDialog : AppCompatDialogFragment() { { CourseManager.getCourses(forceNetwork, it) }, { GroupManager.getFavoriteGroups(it, forceNetwork) } ) - val validCourses = courses.filter { it.isFavorite && it.isValidTerm() && it.hasActiveEnrollment() } + val validCourses = courses.filter { it.isFavorite && it.isValidTerm() && it.hasActiveEnrollment() && it.isNotDeleted() } val courseMap = validCourses.associateBy { it.id } val validGroups = groups.filter { it.courseId == 0L || courseMap[it.courseId] != null } updateCanvasContexts(validCourses, validGroups) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt index c74ef0e33f..16b6f28336 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/AssignmentListFragment.kt @@ -17,16 +17,21 @@ package com.instructure.student.fragment +import android.app.AlertDialog +import android.content.DialogInterface import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView +import androidx.annotation.StringRes import com.google.android.material.appbar.AppBarLayout import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.GradingPeriod +import com.instructure.canvasapi2.utils.Analytics +import com.instructure.canvasapi2.utils.AnalyticsEventConstants import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.interactions.bookmarks.Bookmarkable import com.instructure.interactions.bookmarks.Bookmarker @@ -34,11 +39,14 @@ import com.instructure.interactions.router.Route import com.instructure.interactions.router.RouterParams import com.instructure.pandautils.utils.* import com.instructure.student.R -import com.instructure.student.adapter.AssignmentDateListRecyclerAdapter import com.instructure.student.adapter.TermSpinnerAdapter +import com.instructure.student.adapter.assignment.AssignmentListByDateRecyclerAdapter +import com.instructure.student.adapter.assignment.AssignmentListByTypeRecyclerAdapter +import com.instructure.student.adapter.assignment.AssignmentListRecyclerAdapter import com.instructure.student.interfaces.AdapterToAssignmentsCallback import com.instructure.student.mobius.assignmentDetails.ui.AssignmentDetailsFragment import com.instructure.student.router.RouteMatcher +import com.instructure.student.util.StudentPrefs import kotlinx.android.synthetic.main.assignment_list_layout.* @PageView(url = "{canvasContext}/assignments") @@ -46,17 +54,28 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { private var canvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private lateinit var recyclerAdapter: AssignmentDateListRecyclerAdapter + private lateinit var recyclerAdapter: AssignmentListRecyclerAdapter private var termAdapter: TermSpinnerAdapter? = null + private var sortOrder: AssignmentsSortOrder + get() { + val preferenceKey = StudentPrefs.getString("sortBy_${canvasContext.contextId}", AssignmentsSortOrder.SORT_BY_TIME.preferenceKey) + return AssignmentsSortOrder.fromPreferenceKey(preferenceKey) + } + set(value) { + StudentPrefs.putString("sortBy_${canvasContext.contextId}", value.preferenceKey) + } + private val allTermsGradingPeriod by lazy { - GradingPeriod().apply { title = getString(R.string.allGradingPeriods) } + GradingPeriod().apply { title = getString(R.string.assignmentsListAllGradingPeriods) } } private val adapterToAssignmentsCallback = object : AdapterToAssignmentsCallback { - override fun setTermSpinnerState(isEnabled: Boolean) { - termSpinner?.isEnabled = isEnabled - termAdapter?.isLoading = !isEnabled + override fun assignmentLoadingFinished() { + // If we only have one grading period we want to disable the spinner + val termCount = termAdapter?.count ?: 0 + termSpinner.isEnabled = termCount > 1 + termAdapter?.isLoading = false termAdapter?.notifyDataSetChanged() } @@ -88,11 +107,10 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { inflater.inflate(R.layout.assignment_list_layout, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - recyclerAdapter = AssignmentDateListRecyclerAdapter( - requireContext(), - canvasContext, - adapterToAssignmentsCallback - ) + recyclerAdapter = createRecyclerAdapter() + + sortByTextView.setText(sortOrder.buttonTextRes) + sortByButton.contentDescription = getString(sortOrder.contentDescriptionRes) configureRecyclerView( view, @@ -111,6 +129,41 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { setRefreshingEnabled(false) } }) + + setupSortByButton() + } + + private fun createRecyclerAdapter(): AssignmentListRecyclerAdapter { + return if (sortOrder == AssignmentsSortOrder.SORT_BY_TIME) { + AssignmentListByDateRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback) + } else { + AssignmentListByTypeRecyclerAdapter(requireContext(), canvasContext, adapterToAssignmentsCallback) + } + } + + private fun setupSortByButton() { + sortByButton.onClick { + val checkedItemIndex = sortOrder.index + AlertDialog.Builder(context, R.style.AccentDialogTheme) + .setTitle(R.string.sortByDialogTitle) + .setSingleChoiceItems(R.array.assignmentsSortByOptions, checkedItemIndex, this@AssignmentListFragment::sortOrderSelected) + .setNegativeButton(R.string.sortByDialogCancel) { dialog, _ -> dialog.dismiss() } + .show() + } + } + + private fun sortOrderSelected(dialog: DialogInterface, index: Int) { + dialog.dismiss() + val selectedSortOrder = AssignmentsSortOrder.fromIndex(index) + if (sortOrder != selectedSortOrder) { + sortOrder = selectedSortOrder + recyclerAdapter = createRecyclerAdapter() + listView.adapter = recyclerAdapter + sortByTextView.setText(selectedSortOrder.buttonTextRes) + sortByButton.contentDescription = getString(selectedSortOrder.contentDescriptionRes) + Analytics.logEvent(selectedSortOrder.analyticsKey) + listView.announceForAccessibility(getString(selectedSortOrder.orderSelectedAnnouncement)) + } } override fun applyTheme() { @@ -129,16 +182,19 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } private fun setupGradingPeriods(periods: List) { + val hasGradingPeriods = periods.isNotEmpty() val adapter = TermSpinnerAdapter( requireContext(), android.R.layout.simple_spinner_dropdown_item, - periods + allTermsGradingPeriod + periods + allTermsGradingPeriod, + hasGradingPeriods ) + termSpinner.isEnabled = hasGradingPeriods termAdapter = adapter termSpinner.adapter = adapter termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { - if (adapter.getItem(i)!!.title == getString(R.string.allGradingPeriods)) { + if (adapter.getItem(i)!!.title == getString(R.string.assignmentsListAllGradingPeriods)) { recyclerAdapter.loadAssignment() } else { recyclerAdapter.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true) @@ -153,7 +209,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } // If we have a "current" grading period select it - if (recyclerAdapter.currentGradingPeriod != null) { + if (hasGradingPeriods && recyclerAdapter.currentGradingPeriod != null) { val position = adapter.getPositionForId(recyclerAdapter.currentGradingPeriod?.id ?: 0) if (position != -1) { termSpinner.setSelection(position) @@ -161,8 +217,6 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { toast(R.string.errorOccurred) } } - - termSpinnerLayout.setVisible() } override fun handleBackPressed() = toolbar.closeSearch() @@ -215,3 +269,36 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } } + +enum class AssignmentsSortOrder( + val index: Int, + val preferenceKey: String, + @StringRes val buttonTextRes: Int, + @StringRes val contentDescriptionRes: Int, + @StringRes val orderSelectedAnnouncement: Int, + val analyticsKey: String) { + + SORT_BY_TIME(0, "time", R.string.sortByTime, R.string.a11y_sortByTimeButton, + R.string.a11y_assignmentsSortedByTime, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TIME_SELECTED), + + SORT_BY_TYPE(1, "type", R.string.sortByType, R.string.a11y_sortByTypeButton, + R.string.a11y_assignmentsSortedByType, AnalyticsEventConstants.ASSIGNMENT_LIST_SORT_BY_TYPE_SELECTED); + + companion object { + fun fromPreferenceKey(key: String?): AssignmentsSortOrder { + return when (key) { + SORT_BY_TIME.preferenceKey -> SORT_BY_TIME + SORT_BY_TYPE.preferenceKey -> SORT_BY_TYPE + else -> SORT_BY_TIME // This will be the default value + } + } + + fun fromIndex(key: Int): AssignmentsSortOrder { + return when (key) { + SORT_BY_TIME.index -> SORT_BY_TIME + SORT_BY_TYPE.index -> SORT_BY_TYPE + else -> SORT_BY_TIME // This will be the default value + } + } + } +} 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 7e43381a73..8c53db397f 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 @@ -73,7 +73,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { private var moduleItemId: String by StringArg(key = ITEM_ID) // Default number will get reset - private var NUM_ITEMS = 3 + private var itemsCount = 3 private lateinit var adapter: CourseModuleProgressionAdapter @@ -164,7 +164,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { private val nextItemClickCallback = View.OnClickListener { setupNextModuleName(currentPos) setupNextModule(getModuleItemGroup(currentPos)) - if (currentPos < NUM_ITEMS - 1) { + if (currentPos < itemsCount - 1) { viewPager.currentItem = ++currentPos } updateBottomNavBarButtons() @@ -175,23 +175,20 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { next_item.setOnClickListener(nextItemClickCallback) markDoneButton.setOnClickListener { - if (getModelObject() != null && getModelObject()!!.completionRequirement != null) { - if (getModelObject()!!.completionRequirement!!.completed) { - ModuleManager.markAsNotDone(canvasContext, getModelObject()!!.moduleId, getModelObject()!!.id, + val moduleItem = getModelObject() + if (moduleItem?.completionRequirement != null) { + if (moduleItem.completionRequirement!!.completed) { + ModuleManager.markAsNotDone(canvasContext, moduleItem.moduleId, moduleItem.id, object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - markDoneCheckbox.isChecked = false - getModelObject()!!.completionRequirement!!.completed = false - notifyOfItemChanged(getModelObject()) + setMarkDone(moduleItem, false) } }) } else { - ModuleManager.markAsDone(canvasContext, getModelObject()!!.moduleId, getModelObject()!!.id, + ModuleManager.markAsDone(canvasContext, moduleItem.moduleId, moduleItem.id, object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - markDoneCheckbox.isChecked = true - getModelObject()!!.completionRequirement!!.completed = true - notifyOfItemChanged(getModelObject()) + setMarkDone(moduleItem, true) } }) } @@ -199,6 +196,15 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { } } + private fun setMarkDone(moduleItem: ModuleItem, markDone: Boolean) { + val currentModuleItem = getModelObject() + if (isAdded && moduleItem == currentModuleItem) { + markDoneCheckbox.isChecked = markDone + } + moduleItem.completionRequirement?.completed = markDone + notifyOfItemChanged(moduleItem) + } + private fun setModuleName(name: String) { // Set the label at the bottom moduleName.text = name @@ -209,7 +215,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { // Figure out the total size so the adapter knows how many items it will have var size = 0 for (i in items.indices) { size += items[i].size } - NUM_ITEMS = size + itemsCount = size currentPos = if (bundle != null && bundle.containsKey(Const.MODULE_POSITION)) { bundle.getInt(Const.MODULE_POSITION) @@ -308,7 +314,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { prev_item.setInvisible() } // Don't show the next_item button if we're on the last item - if (currentPosition >= NUM_ITEMS - 1) { + if (currentPosition >= itemsCount - 1) { next_item.visibility = View.INVISIBLE } } @@ -367,7 +373,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { itemsAdded++ } } - NUM_ITEMS += itemsAdded + itemsCount += itemsAdded //only add to currentPos if we're adding to the module that is the previous module //Without this check it will modify the index of the array while we are progressing through @@ -588,7 +594,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { } //endregion - fun getModelObject(): ModuleItem? = getCurrentModuleItem(currentPos) + private fun getModelObject(): ModuleItem? = getCurrentModuleItem(currentPos) //region Adapter inner class CourseModuleProgressionAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { @@ -609,7 +615,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { override fun getItemPosition(`object`: Any): Int = PagerAdapter.POSITION_NONE - override fun getCount(): Int = NUM_ITEMS + override fun getCount(): Int = itemsCount override fun getItem(position: Int): Fragment { expectingUpdate = true @@ -650,7 +656,7 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { } // For RTL - this prevents the scrolling animations (ViewPager doesn't come with RTL support and default page transition animations are backwards) - val pageTransformer = ViewPager.PageTransformer { page, position -> + private val pageTransformer = ViewPager.PageTransformer { page, position -> // Page on right, position = 1 // Page on left, position = -1 // Page on screen, position = 0 @@ -737,7 +743,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { const val MODULE_ITEMS = "module_item" const val MODULE_OBJECTS = "module_objects" - const val MODULE_ID = "module_id" const val MODULE_POSITION = "module_position" const val GROUP_POSITION = "group_position" const val CHILD_POSITION = "child_position" diff --git a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt index 149574e101..98e43fdfd1 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/FileListFragment.kt @@ -450,6 +450,8 @@ class FileListFragment : ParentFragment(), Bookmarkable { private fun animateFabs() = if (mFabOpen) { addFab.startAnimation(fabRotateBackwards) + addFab.announceForAccessibility(getString(R.string.a11y_create_file_folder_gone)) + addFab.contentDescription = getString(R.string.createFileFolderFabContentDesc) addFolderFab.startAnimation(fabHide) addFolderFab.isClickable = false @@ -462,6 +464,8 @@ class FileListFragment : ParentFragment(), Bookmarkable { mFabOpen = false } else { addFab.startAnimation(fabRotateForward) + addFab.announceForAccessibility(getString(R.string.a11y_create_file_folder_visible)) + addFab.contentDescription = getString(R.string.hideCreateFileFolderFabContentDesc) addFolderFab.apply { startAnimation(fabReveal) isClickable = true diff --git a/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt index 9644a1936d..387ab06584 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/ModuleListFragment.kt @@ -129,7 +129,9 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { override fun getSelectedParamName(): String = RouterParams.MODULE_ID fun setupViews() { - recyclerAdapter = ModuleListRecyclerAdapter(canvasContext, requireContext(), object : ModuleAdapterToFragmentCallback { + val navigatingToSpecificModule = !arguments?.getString(MODULE_ID).isNullOrEmpty() + + recyclerAdapter = ModuleListRecyclerAdapter(canvasContext, requireContext(), navigatingToSpecificModule, object : ModuleAdapterToFragmentCallback { override fun onRowClicked(moduleObject: ModuleObject, moduleItem: ModuleItem, position: Int, isOpenDetail: Boolean) { if (moduleItem.type != null && moduleItem.type == ModuleObject.State.UnlockRequirements.apiString) return @@ -162,16 +164,17 @@ class ModuleListFragment : ParentFragment(), Bookmarkable { } else if (recyclerAdapter.size() == 0) { setEmptyView(emptyView, R.drawable.ic_panda_nomodules, R.string.noModules, R.string.noModulesSubtext) } else if (!arguments?.getString(MODULE_ID).isNullOrEmpty()) { - val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString(MODULE_ID)!!.toLong()) - if (groupPosition >= 0) { - // We need to delay scrolling until the expand animation has completed, otherwise modules - // that appear near the end of the list will not have the extra 'expanded' space needed - // to scroll as far as possible toward the top - listView?.postDelayed({ + // We need to delay scrolling until the expand animation has completed, otherwise modules + // that appear near the end of the list will not have the extra 'expanded' space needed + // to scroll as far as possible toward the top + listView?.postDelayed({ + val groupPosition = recyclerAdapter.getGroupItemPosition(arguments!!.getString(MODULE_ID)!!.toLong()) + if (groupPosition >= 0) { val lm = listView?.layoutManager as? LinearLayoutManager lm?.scrollToPositionWithOffset(groupPosition, 0) - }, 1000) - } + arguments?.remove(MODULE_ID) + } + }, 1000) } } }) diff --git a/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt b/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt index 5e0fa80110..4f93b14c0d 100644 --- a/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt +++ b/apps/student/src/main/java/com/instructure/student/interfaces/AdapterToAssignmentsCallback.kt @@ -20,6 +20,6 @@ import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.GradingPeriod interface AdapterToAssignmentsCallback : AdapterToFragmentCallback { - fun setTermSpinnerState(isEnabled: Boolean) + fun assignmentLoadingFinished() fun gradingPeriodsFetched(periods: List) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt index 12666d8646..837d443f1d 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/AssignmentDetailsPresenter.kt @@ -178,7 +178,7 @@ object AssignmentDetailsPresenter : Presenter { + view.iconImageView.setImageResource(R.drawable.ic_media) + view.attachmentNameTextView.text = context.getString(R.string.mediaUpload) + } } view.iconImageView.setColorFilter(tint) view.onClickWithRequireNetwork { onAttachmentClicked(mediaComment.asAttachment()) } diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt index 172a602198..bc4f379c9a 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submissionDetails/drawer/comments/ui/views/CommentSubmissionView.kt @@ -94,9 +94,10 @@ class CommentSubmissionView( } SubmissionType.MEDIA_RECORDING -> { val media = submission.mediaComment ?: throw IllegalStateException("Media comment is null for media submission. WHY!?") - val subtitle = when (media.mediaType!!) { + val subtitle = when (media.mediaType) { MediaComment.MediaType.AUDIO -> context.getString(R.string.commentSubmissionTypeAudio) MediaComment.MediaType.VIDEO -> context.getString(R.string.commentSubmissionTypeVideo) + else -> "" } Triple(R.drawable.ic_media, context.getString(R.string.commentSubmissionTypeMediaFile), subtitle) } diff --git a/apps/student/src/main/res/layout/assignment_list_layout.xml b/apps/student/src/main/res/layout/assignment_list_layout.xml index 648ca2820e..f29117fbe7 100644 --- a/apps/student/src/main/res/layout/assignment_list_layout.xml +++ b/apps/student/src/main/res/layout/assignment_list_layout.xml @@ -36,28 +36,63 @@ android:layout_height="?android:actionBarSize" android:layout_alignParentTop="true" android:background="@color/defaultPrimary" - android:elevation="6dp" app:popupTheme="@style/ToolBarPopupStyle" app:theme="@style/ToolBarStyle" tools:targetApi="lollipop" /> + + + + + + + + + android:layout_centerVertical="true" + android:minWidth="48dp" + android:minHeight="48dp" + android:layout_toStartOf="@id/sortByButton" /> diff --git a/apps/student/src/main/res/layout/fragment_file_list.xml b/apps/student/src/main/res/layout/fragment_file_list.xml index a2f2f21083..0cb84b5a39 100644 --- a/apps/student/src/main/res/layout/fragment_file_list.xml +++ b/apps/student/src/main/res/layout/fragment_file_list.xml @@ -80,6 +80,7 @@ android:contentDescription="@string/createAFolder" android:visibility="invisible" android:layout_marginEnd="16dp" + android:accessibilityTraversalAfter="@id/addFileFab" app:elevation="4dp" app:srcCompat="@drawable/ic_files" /> @@ -95,6 +96,7 @@ android:contentDescription="@string/createAFile" android:visibility="invisible" android:layout_marginEnd="16dp" + android:accessibilityTraversalAfter="@id/addFab" app:elevation="4dp" app:srcCompat="@drawable/ic_file_upload" /> diff --git a/apps/student/src/main/res/layout/fragment_submission_message.xml b/apps/student/src/main/res/layout/fragment_submission_message.xml index 1de944ef59..9f25c75302 100644 --- a/apps/student/src/main/res/layout/fragment_submission_message.xml +++ b/apps/student/src/main/res/layout/fragment_submission_message.xml @@ -56,7 +56,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" - android:textColor="#8B969E" + android:textColor="@color/textLightGray" android:textSize="16sp" android:visibility="gone" app:layout_constraintBottom_toTopOf="@+id/messageTextView" diff --git a/apps/student/src/main/res/layout/term_spinner_view.xml b/apps/student/src/main/res/layout/term_spinner_view.xml index 27fd878ac2..0f09a71dc6 100644 --- a/apps/student/src/main/res/layout/term_spinner_view.xml +++ b/apps/student/src/main/res/layout/term_spinner_view.xml @@ -16,8 +16,8 @@ ~ --> - + tools:text="Grading Period 1" /> + android:layout_weight="0"> - + diff --git a/apps/student/src/main/res/layout/view_comment_submission_attachment.xml b/apps/student/src/main/res/layout/view_comment_submission_attachment.xml index 5caef83629..89051f77f0 100644 --- a/apps/student/src/main/res/layout/view_comment_submission_attachment.xml +++ b/apps/student/src/main/res/layout/view_comment_submission_attachment.xml @@ -19,6 +19,7 @@ android:id="@+id/commentSubmissionAttachmentView" android:layout_width="265dp" android:layout_height="wrap_content" + android:minHeight="48dp" android:background="@drawable/bg_comment_attachment"> + + + + @string/sortByDialogTimeOption + @string/sortByDialogTypeOption + + \ No newline at end of file diff --git a/apps/student/src/main/res/values/styles.xml b/apps/student/src/main/res/values/styles.xml index 4a45d195c9..8c08dea76c 100644 --- a/apps/student/src/main/res/values/styles.xml +++ b/apps/student/src/main/res/values/styles.xml @@ -240,4 +240,9 @@ @color/textLightGray + + diff --git a/apps/student/src/test/java/com/instructure/student/test/adapter/AssignmentDateListRecyclerAdapterTest.kt b/apps/student/src/test/java/com/instructure/student/test/adapter/AssignmentDateListRecyclerAdapterTest.kt deleted file mode 100644 index 18bc345b0b..0000000000 --- a/apps/student/src/test/java/com/instructure/student/test/adapter/AssignmentDateListRecyclerAdapterTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.instructure.student.test.adapter - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.instructure.canvasapi2.models.Assignment -import com.instructure.canvasapi2.utils.toApiString -import com.instructure.student.adapter.AssignmentDateListRecyclerAdapter -import junit.framework.TestCase -import org.junit.Test -import org.junit.runner.RunWith -import java.util.* - -@RunWith(AndroidJUnit4::class) -class AssignmentDateListRecyclerAdapterTest : TestCase() { - - private val itemCallback = AssignmentDateListRecyclerAdapter.itemCallback - - @Test - fun testAreContentsTheSame_sameName() { - val assignment = Assignment() - assignment.name = "Assign1" - TestCase.assertTrue(itemCallback.areContentsTheSame(assignment, assignment)) - } - - @Test - fun testAreContentsTheSame_differentName() { - val assignment1 = Assignment() - assignment1.name = "Assign1" - val assignment2 = Assignment() - assignment2.name = "Assign2" - TestCase.assertFalse(itemCallback.areContentsTheSame(assignment1, assignment2)) - } - - @Test - fun testAreContentsTheSame_oneNullDueDate() { - val assignmentDueDate = Assignment( - name = "Assign1", - dueAt = Date().toApiString() - ) - val assignment1 = Assignment( - name = "Assign1" - ) - TestCase.assertFalse(itemCallback.areContentsTheSame(assignmentDueDate, assignment1)) - TestCase.assertFalse(itemCallback.areContentsTheSame(assignment1, assignmentDueDate)) - TestCase.assertTrue(itemCallback.areContentsTheSame(assignmentDueDate, assignmentDueDate)) - } -} diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/AssignmentDetailsPresenterTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/AssignmentDetailsPresenterTest.kt index 800d9ccdcb..cada0bda39 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/AssignmentDetailsPresenterTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/AssignmentDetailsPresenterTest.kt @@ -316,7 +316,7 @@ class AssignmentDetailsPresenterTest : Assert() { fun `Uses gray color for non-submitted status`() { val model = baseModel.copy(assignmentResult = DataResult.Success(baseAssignment)) val state = AssignmentDetailsPresenter.present(model, context) as AssignmentDetailsViewState.Loaded - assertEquals(0xFF8B969E.toInt(), state.submittedStateColor) + assertEquals(0xFF556572.toInt(), state.submittedStateColor) } @Test diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt index fd1f6eb11a..ce6d879884 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/GradeCellStateTest.kt @@ -193,7 +193,7 @@ class GradeCellStateTest : Assert() { score = 0.0 ) val expected = baseGradedState.copy( - accentColor = 0xFF8B969E.toInt(), + accentColor = 0xFF556572.toInt(), graphPercent = 1.0f, showIncompleteIcon = true, grade = "Incomplete", diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt index f6544cca7d..1322413cf2 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/SyllabusPageTest.kt @@ -37,9 +37,6 @@ class SyllabusPageTest : TeacherTest() { // Tests that we can open an assignment from the syllabus/summary, and does some verification of the calendar event. @Test fun testSyllabus_openAssignmentDetails() { - // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. - Thread.sleep(3000) - RemoteConfigPrefs.putString(RemoteConfigParam.SHOW_TEACHER_SYLLABUS.rc_name, "true") val data = goToSyllabus(eventCount = 0, assignmentCount = 1) val assignment = data.assignments.values.first() @@ -53,9 +50,6 @@ class SyllabusPageTest : TeacherTest() { // Tests that we can open a calendar event from the syllabus/summary, and does some verification of the calendar event. @Test fun testSyllabus_openCalendarEvent() { - // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. - Thread.sleep(3000) - RemoteConfigPrefs.putString(RemoteConfigParam.SHOW_TEACHER_SYLLABUS.rc_name, "true") val data = goToSyllabus(eventCount = 1, assignmentCount = 0) val course = data.courses.values.first() @@ -71,9 +65,6 @@ class SyllabusPageTest : TeacherTest() { // Tests that we can open the edit syllabus. @Test fun testSyllabus_openEditSyllabus() { - // We have to add this delay to be sure that the remote config is already fetched before we want to override remote config values. - Thread.sleep(3000) - RemoteConfigPrefs.putString(RemoteConfigParam.SHOW_TEACHER_SYLLABUS.rc_name, "true") goToSyllabus(eventCount = 0, assignmentCount = 1) syllabusPage.openEditSyllabus() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt new file mode 100644 index 0000000000..e304024a76 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt @@ -0,0 +1,93 @@ +package com.instructure.teacher.ui.e2e + +import androidx.test.espresso.Espresso +import com.instructure.canvas.espresso.E2E +import com.instructure.dataseeding.api.ConversationsApi +import com.instructure.dataseeding.api.GroupsApi +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.teacher.ui.utils.TeacherTest +import com.instructure.teacher.ui.utils.seedData +import com.instructure.teacher.ui.utils.tokenLogin +import org.junit.Test + +class InboxE2ETest : TeacherTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() { + //We dont want to see accessibility errors on E2E tests + } + + @E2E + @Test + @TestMetaData(Priority.P0, FeatureCategory.INBOX, TestCategory.E2E) + fun testInboxE2E() { + val data = seedData(students = 1, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student1 = data.studentsList[0] + + val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) + val group = GroupsApi.createGroup(groupCategory.id, teacher.token) + GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) + + tokenLogin(teacher) + dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course = course) + dashboardPage.openInbox() + inboxPage.assertInboxEmpty() + + val seedConversation = ConversationsApi.createConversation( + token = student1.token, + recipients = listOf(teacher.id.toString()) + ) + + inboxPage.refresh() + inboxPage.assertHasConversation() + inboxPage.clickConversation(seedConversation[0]) + + inboxMessagePage.clickReply() + addMessagePage.addReply("Hello there") + inboxMessagePage.assertHasReply() + } + + @E2E + @Test + @TestMetaData(Priority.P0, FeatureCategory.INBOX, TestCategory.E2E) + fun testInboxNewMessageE2E() { + val data = seedData(students = 2, teachers = 1, courses = 1) + val teacher = data.teachersList[0] + val course = data.coursesList[0] + val student1 = data.studentsList[0] + val student2 = data.studentsList[1] + + val groupCategory = GroupsApi.createCourseGroupCategory(course.id, teacher.token) + val group = GroupsApi.createGroup(groupCategory.id, teacher.token) + GroupsApi.createGroupMembership(group.id, student1.id, teacher.token) + + tokenLogin(teacher) + dashboardPage.waitForRender() + dashboardPage.assertDisplaysCourse(course = course) + dashboardPage.openInbox() + inboxPage.assertInboxEmpty() + + inboxPage.clickAddMessageFAB() + addMessagePage.clickCourseSpinner() + addMessagePage.selectCourseFromSpinner(courseName = course.name) + addMessagePage.clickAddContacts() + chooseRecipientsPage.clickStudentCategory() + chooseRecipientsPage.clickStudent(student = student1) + chooseRecipientsPage.clickStudent(student = student2) + chooseRecipientsPage.clickDone() + addMessagePage.addSubject(subject = "Hello there") + addMessagePage.addMessage("General Kenobi") + addMessagePage.clickSendButton() + inboxPage.filterInbox("Sent") + inboxPage.assertHasConversation() + inboxPage.clickConversation(conversationSubject = "Hello there") + Espresso.pressBack() + inboxPage.assertHasConversation() + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt new file mode 100644 index 0000000000..2537576e55 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/TodoE2ETest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.teacher.ui.e2e + +import com.instructure.canvas.espresso.E2E +import com.instructure.dataseeding.api.SubmissionsApi +import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.util.days +import com.instructure.dataseeding.util.fromNow +import com.instructure.dataseeding.util.iso8601 +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.teacher.ui.utils.* +import org.junit.Test +import java.util.* + +class TodoE2ETest : TeacherTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() { + //We dont want to see accessibility errors on E2E tests + } + + @E2E + @Test + @TestMetaData(Priority.P0, FeatureCategory.TODOS, TestCategory.E2E) + fun testTodoE2E() { + // Inherited from student todo tests, may check this out later + // Don't attempt this test on a Friday, Saturday or Sunday. + // The TODO tab doesn't seem to behave correctly on Fridays (or presumably weekends). + val dayOfWeek = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) + if(dayOfWeek == Calendar.FRIDAY || dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) { + println("We don't run the TODO E2E test on weekends") + return + } + + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + val assignment = seedAssignments( + courseId = course.id, + dueAt = 1.days.fromNow.iso8601, + submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY), + teacherToken = teacher.token, + pointsPossible = 15.0 + ) + + seedAssignmentSubmission( + submissionSeeds = listOf(SubmissionsApi.SubmissionSeedInfo( + amount = 1, + submissionType = SubmissionType.ONLINE_TEXT_ENTRY + )), + assignmentId = assignment[0].id, + courseId = course.id, + studentToken = student.token + ) + + tokenLogin(teacher) + + dashboardPage.waitForRender() + dashboardPage.openTodo() + todoPage.waitForRender() + todoPage.assertTodoElementIsDisplayed(courseName = course.name) + + SubmissionsApi.gradeSubmission( + teacherToken = teacher.token, + courseId = course.id, + assignmentId = assignment[0].id, + studentId = student.id, + postedGrade = "15", + excused = false + ) + + todoPage.refresh() + todoPage.assertEmptyView() + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AddMessagePage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AddMessagePage.kt index 3eecce86d9..47dcce1596 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AddMessagePage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/AddMessagePage.kt @@ -59,7 +59,11 @@ class AddMessagePage: BasePage() { } fun selectCourseFromSpinner(course: Course) { - waitForViewWithText(course.name).click() + selectCourseFromSpinner(course.name) + } + + fun selectCourseFromSpinner(courseName: String) { + waitForViewWithText(courseName).click() } fun clickAddContacts() { @@ -80,4 +84,17 @@ class AddMessagePage: BasePage() { messageEditText.replaceText(message) sendButton.click() } + + fun addSubject(subject: String) { + editSubjectEditText.replaceText(subject) + } + + fun addMessage(message: String) { + messageEditText.scrollTo() + messageEditText.replaceText(message) + } + + fun clickSendButton() { + sendButton.click() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ChooseRecipientsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ChooseRecipientsPage.kt index efc2ed3f7b..484f0b7a5d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ChooseRecipientsPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/ChooseRecipientsPage.kt @@ -53,4 +53,8 @@ class ChooseRecipientsPage: BasePage() { fun clickStudent(student: User) { waitForViewWithText(student.shortName!!).click() } + + fun clickStudent(student: CanvasUserApiModel) { + waitForViewWithText(student.shortName).click() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt index 8fc296b620..41d766cf7e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/DashboardPage.kt @@ -90,4 +90,12 @@ class DashboardPage : BasePage() { fun waitForRender() { onView(hamburgerButtonMatcher).waitForCheck(matches(isDisplayed())) } + + fun openInbox() { + inboxTab.click() + } + + fun openTodo() { + todoTab.click() + } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt index 8a8a53fe4d..25dd44321e 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt @@ -4,7 +4,9 @@ import com.instructure.canvasapi2.models.Conversation import com.instructure.dataseeding.model.ConversationApiModel import com.instructure.espresso.* import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView import com.instructure.espresso.page.waitForViewWithText +import com.instructure.espresso.page.withId import com.instructure.teacher.R import com.instructure.teacher.ui.utils.WaitForToolbarTitle @@ -29,14 +31,31 @@ class InboxPage: BasePage() { } fun clickConversation(conversation: ConversationApiModel) { - waitForViewWithText(conversation.subject).click() + clickConversation(conversation.subject) } fun clickConversation(conversation: Conversation) { - waitForViewWithText(conversation.subject!!).click() + clickConversation(conversation.subject!!) + } + + fun clickConversation(conversationSubject: String) { + waitForViewWithText(conversationSubject).click() } fun clickAddMessageFAB() { addMessageFAB.click() } + + fun assertInboxEmpty() { + onView(withId(R.id.emptyPandaView)).assertDisplayed() + } + + fun refresh() { + onView(withId(R.id.swipeRefreshLayout)).swipeDown() + } + + fun filterInbox(filterFor: String) { + onView(withId(R.id.filterButton)).click() + waitForViewWithText(filterFor).click() + } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt new file mode 100644 index 0000000000..beb0a8422d --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/TodoPage.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.teacher.ui.pages + +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.page.* +import com.instructure.espresso.swipeDown +import com.instructure.teacher.R + +class TodoPage : BasePage() { + + fun waitForRender() { + onView(withId(R.id.toDoToolbar)).assertDisplayed() + } + + fun assertTodoElementIsDisplayed(courseName: String) { + onView(withId(R.id.toDoCourse) + withText(courseName)).assertDisplayed() + } + + fun refresh() { + onView(withId(R.id.swipeRefreshLayout)).swipeDown() + } + + fun assertEmptyView() { + onView(withId(R.id.emptyPandaView)).assertDisplayed() + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 5d13627fa8..a64a0e239d 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -39,48 +39,49 @@ abstract class TeacherTest : CanvasTest() { /** * Required for auto complete of page objects within tests */ - val coursesListPage = CoursesListPage() + val addMessagePage = AddMessagePage() val allCoursesListPage = AllCoursesListPage() - val assignmentListPage = AssignmentListPage() - val assignmentSubmissionListPage = AssignmentSubmissionListPage() + val announcementsListPage = AnnouncementsListPage() + val assigneeListPage = AssigneeListPage() val assignmentDetailsPage = AssignmentDetailsPage() val assignmentDueDatesPage = AssignmentDueDatesPage() + val assignmentListPage = AssignmentListPage() + val assignmentSubmissionListPage = AssignmentSubmissionListPage() + val calendarEventPage = CalendarEventPage() + val chooseRecipientsPage = ChooseRecipientsPage() val courseBrowserPage = CourseBrowserPage() - val editCoursesListPage = EditCoursesListPage() val courseSettingsPage = CourseSettingsPage() + val coursesListPage = CoursesListPage() + val dashboardPage = DashboardPage() + val discussionsListPage = DiscussionsListPage() + val editAnnouncementPage = EditAnnouncementPage() val editAssignmentDetailsPage = EditAssignmentDetailsPage() - val assigneeListPage = AssigneeListPage() - val loginLandingPage = LoginLandingPage() + val editCoursesListPage = EditCoursesListPage() + val editPageDetailsPage = EditPageDetailsPage() + val editQuizDetailsPage = EditQuizDetailsPage() + val editSyllabusPage = EditSyllabusPage() + val inboxMessagePage = InboxMessagePage() + val inboxPage = InboxPage() val loginFindSchoolPage = LoginFindSchoolPage() + val loginLandingPage = LoginLandingPage() val loginSignInPage = LoginSignInPage() - val notATeacherPage = NotATeacherPage() - val inboxPage = InboxPage() + val modulesPage = ModulesPage() val navDrawerPage = NavDrawerPage() - val speedGraderPage = SpeedGraderPage() - val speedGraderGradePage = SpeedGraderGradePage() - val speedGraderCommentsPage = SpeedGraderCommentsPage() - val speedGraderFilesPage = SpeedGraderFilesPage() - val quizListPage = QuizListPage() + val notATeacherPage = NotATeacherPage() + val pageListPage = PageListPage() + val peopleListPage = PeopleListPage() val quizDetailsPage = QuizDetailsPage() - val editQuizDetailsPage = EditQuizDetailsPage() - val discussionsListPage = DiscussionsListPage() + val quizListPage = QuizListPage() val quizSubmissionListPage = QuizSubmissionListPage() - val inboxMessagePage = InboxMessagePage() - val addMessagePage = AddMessagePage() - val chooseRecipientsPage = ChooseRecipientsPage() + val speedGraderCommentsPage = SpeedGraderCommentsPage() + val speedGraderFilesPage = SpeedGraderFilesPage() + val speedGraderGradePage = SpeedGraderGradePage() + val speedGraderPage = SpeedGraderPage() val speedGraderQuizSubmissionPage = SpeedGraderQuizSubmissionPage() - val webViewLoginPage = WebViewLoginPage() - val announcementsListPage = AnnouncementsListPage() - val peopleListPage = PeopleListPage() val studentContextPage = StudentContextPage() - val pageListPage = PageListPage() val syllabusPage = SyllabusPage() - val calendarEventPage = CalendarEventPage() - val dashboardPage = DashboardPage() - val editSyllabusPage = EditSyllabusPage() - val editPageDetailsPage = EditPageDetailsPage() - val modulesPage = ModulesPage() - val editAnnouncementPage = EditAnnouncementPage() + val todoPage = TodoPage() + val webViewLoginPage = WebViewLoginPage() } diff --git a/apps/teacher/src/main/AndroidManifest.xml b/apps/teacher/src/main/AndroidManifest.xml index d0c0a8c17a..7b516da8e5 100644 --- a/apps/teacher/src/main/AndroidManifest.xml +++ b/apps/teacher/src/main/AndroidManifest.xml @@ -43,7 +43,6 @@ android:largeHeap="true" android:supportsRtl="true" android:theme="@style/AppTheme" - android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true" android:networkSecurityConfig="@xml/network_security_config" tools:replace="android:icon"> diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt index 5c8f0dbe79..8d787f8e3b 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/CourseBrowserFragment.kt @@ -77,7 +77,6 @@ class CourseBrowserFragment : BaseSyncFragment< override val recyclerView: RecyclerView get() = courseBrowserRecyclerView override fun withPagination() = false override fun getPresenterFactory() = CourseBrowserPresenterFactory(mCanvasContext) { tab, attendanceId -> - val showSyllabus = RemoteConfigUtils.getBoolean(RemoteConfigParam.SHOW_TEACHER_SYLLABUS) //Filter for white-list supported features //TODO: support other things like it.isHidden when(tab.tabId) { @@ -89,8 +88,8 @@ class CourseBrowserFragment : BaseSyncFragment< Tab.FILES_ID, Tab.PAGES_ID, Tab.MODULES_ID, - Tab.STUDENT_VIEW -> true - Tab.SYLLABUS_ID -> showSyllabus + Tab.STUDENT_VIEW, + Tab.SYLLABUS_ID -> true else -> { if (attendanceId != 0L && tab.tabId.endsWith(attendanceId.toString())) { TeacherPrefs.attendanceExternalToolId = tab.tabId diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt index af3aa23f9b..47cecb3246 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditAssignmentDetailsFragment.kt @@ -293,6 +293,7 @@ class EditAssignmentDetailsFragment : BaseFragment() { sectionsMapped += sections.associateBy { it.id } studentsMapped += students.associateBy { it.id } } + setupAddOverrideButton() setupOverrides() if (mScrollToDates) { @@ -314,6 +315,10 @@ class EditAssignmentDetailsFragment : BaseFragment() { } } } + } + + private fun setupAddOverrideButton() { + addOverride.setVisible(true) // Theme add button and plus image addOverrideText.setTextColor(ThemePrefs.buttonColor) @@ -342,7 +347,10 @@ class EditAssignmentDetailsFragment : BaseFragment() { dueDateGroup.groupIds.forEach { assignees.add(groupsMapped[it]?.name!!) } dueDateGroup.sectionIds.forEach { assignees.add(sectionsMapped[it]?.name!!) } dueDateGroup.studentIds.forEach { - assignees.add(studentsMapped[it]!!.let { user -> Pronouns.span(user.name, user.pronouns) }) + val student = studentsMapped[it] + if (student != null) { + assignees.add(student.let { user -> Pronouns.span(user.name, user.pronouns) }) + } } v.setupOverride(index, dueDateGroup, mEditDateGroups.size > 1, assignees, datePickerOnClick, timePickerOnClick, removeOverrideClick) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt index 3a0d1c5f9a..1d8f123096 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/EditQuizDetailsFragment.kt @@ -301,7 +301,6 @@ class EditQuizDetailsFragment : BasePresenterFragment< if(mQuizType == null) { mQuizType = quizType } - updateOverridesForQuizType() setupPublishSwitch() setupAccessCodeSwitch() @@ -315,20 +314,6 @@ class EditQuizDetailsFragment : BasePresenterFragment< with(presenter) {if (mEditDateGroups.isEmpty()) mEditDateGroups.addAll(mAssignment.groupedDueDates)} presenter.getStudentsGroupsAndSections() - - // Theme add button and plus image - addOverrideText.setTextColor(ThemePrefs.buttonColor) - plus.setColorFilter(ThemePrefs.buttonColor) - - addOverride.setOnClickListener { - presenter.mEditDateGroups.add(DueDateGroup()) - setupOverrides() - scrollView.post { - scrollView.fullScroll(ScrollView.FOCUS_DOWN) - } - // This opens the assignees page to save the user a click. - overrideContainer.descendants().last().assignTo.performClick() - } } private fun setupDescription(quiz: Quiz) { @@ -374,7 +359,10 @@ class EditQuizDetailsFragment : BasePresenterFragment< dueDateGroup.groupIds.forEach { assignees.add(groupsMapped[it]?.name ?: "") } dueDateGroup.sectionIds.forEach { assignees.add(sectionsMapped[it]?.name ?: "") } dueDateGroup.studentIds.forEach { - assignees.add(studentsMapped[it]!!.let { user -> Pronouns.span(user.name, user.pronouns) }) + val student = studentsMapped[it] + if (student != null) { + assignees.add(student.let { user -> Pronouns.span(user.name, user.pronouns) }) + } } v.setupOverride(index, dueDateGroup, mEditDateGroups.size > 1, assignees, datePickerOnClick, timePickerOnClick, removeOverrideClick) { @@ -464,6 +452,24 @@ class EditQuizDetailsFragment : BasePresenterFragment< toast(R.string.error_saving_quiz) } + override fun setupAddOverridesButton() { + updateOverridesForQuizType() + + // Theme add button and plus image + addOverrideText.setTextColor(ThemePrefs.buttonColor) + plus.setColorFilter(ThemePrefs.buttonColor) + + addOverride.setOnClickListener { + presenter.mEditDateGroups.add(DueDateGroup()) + setupOverrides() + scrollView.post { + scrollView.fullScroll(ScrollView.FOCUS_DOWN) + } + // This opens the assignees page to save the user a click. + overrideContainer.descendants().last().assignTo.performClick() + } + } + override val identity: Long? get() = if(mQuizId != 0L) mQuizId else mQuiz.id override val skipCheck: Boolean get() = false diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt index 09452c62b5..ee6f12a951 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/FileListFragment.kt @@ -291,6 +291,7 @@ class FileListFragment : BaseSyncFragment< private fun animateFabs() = if (fabOpen) { addFab.startAnimation(fabRotateBackwards) + addFab.announceForAccessibility(getString(R.string.a11y_create_file_folder_gone)) addFab.contentDescription = getString(R.string.createFileFolderFabContentDesc) addFolderFab.startAnimation(fabHide) addFolderFab.isClickable = false @@ -304,6 +305,8 @@ class FileListFragment : BaseSyncFragment< fabOpen = false } else { addFab.startAnimation(fabRotateForward) + addFab.announceForAccessibility(getString(R.string.a11y_create_file_folder_visible)) + addFab.contentDescription = getString(R.string.hideCreateFileFolderFabContentDesc) addFolderFab.apply { startAnimation(fabReveal) isClickable = true diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt index 4095551ac1..43579db069 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/SpeedGraderTextSubmissionFragment.kt @@ -75,7 +75,7 @@ class SpeedGraderTextSubmissionFragment : Fragment(), SpeedGraderWebNavigator { override fun shouldLaunchInternalWebViewFragment(url: String): Boolean = true } - textSubmissionWebView.loadHtml(mSubmissionText, mSubmissionText) + textSubmissionWebView.loadHtml(mSubmissionText, getString(R.string.a11y_submissionText)) } override fun onStop() { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/EditQuizDetailsPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/EditQuizDetailsPresenter.kt index 0c2e8891ae..589b282f60 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/EditQuizDetailsPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/EditQuizDetailsPresenter.kt @@ -80,6 +80,7 @@ class EditQuizDetailsPresenter(var mQuiz: Quiz, var mAssignment: Assignment, val studentsMapped += students.associateBy { it.id } } + viewCallback?.setupAddOverridesButton() viewCallback?.setupOverrides() viewCallback?.scrollCheck() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index 119860da21..676ec5e1b7 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -72,14 +72,7 @@ object RouteMatcher : BaseRouteMatcher() { routes.add(Route(courseOrGroup("/"), CoursesFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id"), CourseBrowserFragment::class.java)) - val showSyllabus = RemoteConfigUtils.getBoolean(RemoteConfigParam.SHOW_TEACHER_SYLLABUS) - val syllabusRoute = if (showSyllabus) { - Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusFragment::class.java) - } else { - // We don't want to route to the syllabus, but this needs to be above the other assignments routing so it catches here first - Route(courseOrGroup("/:course_id/assignments/syllabus"), RouteContext.DO_NOT_ROUTE) - } - routes.add(syllabusRoute) + routes.add(Route(courseOrGroup("/:course_id/assignments/syllabus"), SyllabusFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/assignments"), AssignmentListFragment::class.java)) routes.add(Route(courseOrGroup("/:course_id/assignments/:assignment_id"), AssignmentListFragment::class.java, AssignmentDetailsFragment::class.java)) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/services/FileDownloadService.kt b/apps/teacher/src/main/java/com/instructure/teacher/services/FileDownloadService.kt index fed957c621..d56f257d1e 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/services/FileDownloadService.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/services/FileDownloadService.kt @@ -50,7 +50,7 @@ class FileDownloadService @JvmOverloads constructor(name: String = FileUploadSer private var isCanceled = false private var url = "" private var fileName = "" - private lateinit var notificationBuilder: NotificationCompat.Builder + private var notificationBuilder: NotificationCompat.Builder? = null private lateinit var file: File private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } @@ -271,20 +271,20 @@ class FileDownloadService @JvmOverloads constructor(name: String = FileUploadSer .setSmallIcon(android.R.drawable.stat_sys_download) .setProgress(0, 0, true) - startForeground(NOTIFICATION_ID, notificationBuilder.build()) + startForeground(NOTIFICATION_ID, notificationBuilder?.build()) } private fun updateNotificationError(message: String) { - notificationBuilder.setContentText(message).setProgress(0, 0, false) - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + notificationBuilder?.setContentText(message)?.setProgress(0, 0, false) + notificationManager.notify(NOTIFICATION_ID, notificationBuilder?.build()) } private fun updateNotificationComplete(routeInternally: Boolean = true) { - notificationBuilder.setProgress(0, 0, false) - .setContentTitle(getString(R.string.fileDownloadedSuccessfully)) - .setContentText(fileName) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setAutoCancel(true) + notificationBuilder?.setProgress(0, 0, false) + ?.setContentTitle(getString(R.string.fileDownloadedSuccessfully)) + ?.setContentText(fileName) + ?.setSmallIcon(android.R.drawable.stat_sys_download_done) + ?.setAutoCancel(true) if (!routeInternally) { // This is here specifically for Ark video downloads @@ -296,7 +296,7 @@ class FileDownloadService @JvmOverloads constructor(name: String = FileUploadSer addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } val pendingIntent = PendingIntent.getActivity(this@FileDownloadService, 0, contentIntent, 0) - notificationBuilder.setContentIntent(pendingIntent) + notificationBuilder?.setContentIntent(pendingIntent) } else { // All other downloads val intent = RouteValidatorActivity.createIntent(this, Uri.parse(url)) @@ -305,11 +305,11 @@ class FileDownloadService @JvmOverloads constructor(name: String = FileUploadSer intent.putExtras(bundle) val contentIntent = PendingIntent.getActivity(this@FileDownloadService, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT) - notificationBuilder.setContentIntent(contentIntent) + notificationBuilder?.setContentIntent(contentIntent) } - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + notificationManager.notify(NOTIFICATION_ID, notificationBuilder?.build()) } @@ -319,8 +319,10 @@ class FileDownloadService @JvmOverloads constructor(name: String = FileUploadSer if (isCanceled) { notificationManager.cancel(NOTIFICATION_ID) } else { - notificationBuilder.setOngoing(false) - notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + if (notificationBuilder != null) { + notificationBuilder?.setOngoing(false) + notificationManager.notify(NOTIFICATION_ID, notificationBuilder?.build()) + } } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/CommentMediaAttachmentView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/CommentMediaAttachmentView.kt index db138465be..b66b5d50b6 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/CommentMediaAttachmentView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/CommentMediaAttachmentView.kt @@ -41,6 +41,10 @@ class CommentMediaAttachmentView(context: Context, mediaComment: MediaComment, o view.iconImageView.setImageResource(R.drawable.ic_media) view.attachmentNameTextView.text = context.getString(R.string.mediaUploadVideo) } + else -> { + view.iconImageView.setImageResource(R.drawable.ic_media) + view.attachmentNameTextView.text = context.getString(R.string.mediaUpload) + } } view.onClickWithRequireNetwork { onAttachmentClicked(mediaComment.asAttachment()) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/CommentSubmissionView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/CommentSubmissionView.kt index b6ad936d80..a2d9e1a2ef 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/CommentSubmissionView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/CommentSubmissionView.kt @@ -70,9 +70,10 @@ class CommentSubmissionView(context: Context, val submission: Submission) : Line } SubmissionType.MEDIA_RECORDING -> { val media = submission.mediaComment ?: throw IllegalStateException("Media comment is null for media submission. WHY!?") - val subtitle = when (media.mediaType!!) { + val subtitle = when (media.mediaType) { MediaComment.MediaType.AUDIO -> context.getString(R.string.submissionTypeAudio) MediaComment.MediaType.VIDEO -> context.getString(R.string.submissionTypeVideo) + else -> "" } Triple(R.drawable.ic_media, context.getString(R.string.speedGraderMediaFile), subtitle) } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/EditQuizDetailsView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/EditQuizDetailsView.kt index 249171c9d1..65ca9b9a12 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/EditQuizDetailsView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/EditQuizDetailsView.kt @@ -25,4 +25,5 @@ interface EditQuizDetailsView : FragmentViewInterface { fun errorSavingQuiz() fun quizSavedSuccessfully() fun populateQuizDetails() + fun setupAddOverridesButton() } diff --git a/apps/teacher/src/main/res/layout/adapter_course_browser.xml b/apps/teacher/src/main/res/layout/adapter_course_browser.xml index 640c774b66..a03893fa56 100644 --- a/apps/teacher/src/main/res/layout/adapter_course_browser.xml +++ b/apps/teacher/src/main/res/layout/adapter_course_browser.xml @@ -40,7 +40,7 @@ android:layout_height="wrap_content" android:layout_marginStart="32dp" android:layout_marginEnd="16dp" - android:textColor="#8B969E" + android:textColor="@color/textLightGray" android:textSize="14sp" tools:text="This is a description for this item"/> diff --git a/apps/teacher/src/main/res/layout/fragment_course_browser_empty.xml b/apps/teacher/src/main/res/layout/fragment_course_browser_empty.xml index 70b6a6ed44..ccd563bcd2 100644 --- a/apps/teacher/src/main/res/layout/fragment_course_browser_empty.xml +++ b/apps/teacher/src/main/res/layout/fragment_course_browser_empty.xml @@ -65,7 +65,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="4dp" - android:textColor="#8B969E" + android:textColor="@color/textLightGray" android:textSize="16sp" tools:text="Spring 2016" /> diff --git a/apps/teacher/src/main/res/layout/fragment_edit_assignment_details.xml b/apps/teacher/src/main/res/layout/fragment_edit_assignment_details.xml index 141522d39f..4740f08883 100644 --- a/apps/teacher/src/main/res/layout/fragment_edit_assignment_details.xml +++ b/apps/teacher/src/main/res/layout/fragment_edit_assignment_details.xml @@ -234,7 +234,8 @@ android:layout_height="wrap_content" android:minHeight="48dp" android:layout_marginBottom="16dp" - android:layout_marginStart="10dp"> + android:layout_marginStart="10dp" + android:visibility="gone"> + android:layout_marginStart="10dp" + android:visibility="gone"> diff --git a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml index 4b903cb083..dd6638c3f2 100644 --- a/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml +++ b/apps/teacher/src/main/res/layout/fragment_not_a_teacher.xml @@ -38,7 +38,7 @@ android:layout_marginStart="24dp" android:gravity="center" android:text="@string/not_a_teacher_tap_to_visit_play_store" - android:textColor="#8B969E" + android:textColor="@color/textLightGray" android:textSize="16sp"/> diff --git a/apps/teacher/src/main/res/layout/fragment_speedgrader_empty.xml b/apps/teacher/src/main/res/layout/fragment_speedgrader_empty.xml index b8dd42b51d..9353f7c5d9 100644 --- a/apps/teacher/src/main/res/layout/fragment_speedgrader_empty.xml +++ b/apps/teacher/src/main/res/layout/fragment_speedgrader_empty.xml @@ -55,7 +55,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" - android:textColor="#8B969E" + android:textColor="@color/textLightGray" android:textSize="16sp" android:visibility="gone" app:layout_constraintBottom_toTopOf="@+id/messageTextView" diff --git a/apps/teacher/src/main/res/menu/menu_compose_message_activity.xml b/apps/teacher/src/main/res/menu/menu_compose_message_activity.xml index 7c69c705e1..74d41da28e 100644 --- a/apps/teacher/src/main/res/menu/menu_compose_message_activity.xml +++ b/apps/teacher/src/main/res/menu/menu_compose_message_activity.xml @@ -22,7 +22,7 @@ android:icon="@drawable/ic_attachment" android:title="@string/attachment" android:visible="false" - android:tint="#8B969E" + android:tint="@color/discussionLiking" app:showAsAction="ifRoom" /> > - @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[]=completed&state[]=available") + @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 firstPageCourses: 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") @@ -139,14 +139,6 @@ object CourseAPI { callback.addCall(adapter.build(CoursesInterface::class.java, params).next(nextUrl)).enqueue(callback) } - fun getCourses(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { - if (StatusCallback.isFirstPage(callback.linkHeaders)) { - callback.addCall(adapter.build(CoursesInterface::class.java, params).firstPageCourses).enqueue(callback) - } else if (callback.linkHeaders != null && StatusCallback.moreCallsExist(callback.linkHeaders)) { - callback.addCall(adapter.build(CoursesInterface::class.java, params).next(callback.linkHeaders!!.nextUrl!!)).enqueue(callback) - } - } - fun getDashboardCourses(adapter: RestBuilder, callback: StatusCallback>, params: RestParams) { callback.addCall(adapter.build(CoursesInterface::class.java, params).dashboardCourses).enqueue(callback) } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt index 8e39b56b2c..f494d2048d 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/EnrollmentAPI.kt @@ -31,6 +31,7 @@ object EnrollmentAPI { const val STATE_ACTIVE = "active" const val STATE_INVITED = "invited" + const val STATE_CURRENT_AND_FUTURE = "current_and_future" internal interface EnrollmentInterface { 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 d22531b7f7..70c84d9052 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 @@ -26,10 +26,13 @@ import com.instructure.canvasapi2.models.postmodels.UpdateCourseWrapper import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.ExhaustiveListCallback +import com.instructure.canvasapi2.utils.isNotDeleted import com.instructure.canvasapi2.utils.weave.apiAsync import kotlinx.coroutines.Deferred import java.io.IOException import java.util.* +import kotlin.collections.ArrayList +import kotlin.jvm.Throws object CourseManager { @@ -279,7 +282,7 @@ object CourseManager { val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceNetwork) val data = CourseAPI.getCoursesSynchronously(adapter, params) - return data ?: ArrayList() + return data?.filter { it.isNotDeleted() } ?: ArrayList() } fun createCourseMap(courses: List?): Map = courses?.associateBy { it.id } ?: emptyMap() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt index f730530a6d..7264ee0c36 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/ToDoManager.kt @@ -82,7 +82,7 @@ object ToDoManager { // Return combined list, sorted by date val defaultDate = Date(0) - return (todos + events).sortedBy{ it.assignment?.dueDate ?: defaultDate } + return (todos + events).sortedBy{ it.comparisonDate ?: defaultDate } } } 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 f17e4a124d..dfc23db699 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 @@ -206,24 +206,18 @@ data class Course( * * Useful for setting content to read-only, such as submissions */ - fun isReadOnlyForCurrentDate(): Boolean { + fun isBetweenValidDateRange(): Boolean { val now = Date() if (accessRestrictedByDate) return false if (workflowState == "completed") return false - val isValidForCourse = isWithinDates( - startAt.toDate(), - endAt.toDate(), - now - ) - - return if (restrictEnrollmentsToCourseDate && !isValidForCourse) { - false + return if (restrictEnrollmentsToCourseDate) { + isWithinDates(startAt.toDate(), endAt.toDate(), now) } else { val isValidForTerm = isWithinDates(term?.startDate, term?.endDate, now) - if(isValidForTerm) { + if (isValidForTerm) { // check the sections if (sections.isEmpty()) { true diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/MediaComment.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/MediaComment.kt index 5e6ef77a29..96a5799bbb 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/MediaComment.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/MediaComment.kt @@ -38,10 +38,10 @@ data class MediaComment( ) : Parcelable { enum class MediaType { - @SerializedName("audio") + @SerializedName("audio", alternate = ["audio/*"]) AUDIO, - @SerializedName("video") + @SerializedName("video", alternate = ["video/*"]) VIDEO } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt index f92def30e6..413dd52873 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/Analytics.kt @@ -61,8 +61,10 @@ object AnalyticsEventConstants { const val ASSIGNMENT_DETAIL_QUIZLAUNCH = "assignment_detail_quizlaunch" const val ASSIGNMENT_DETAIL_DISCUSSION = "assignment_detail_discussion" const val ASSIGNMENT_DETAIL_DISCUSSIONLAUNCH = "assignment_detail_discussionlaunch" - const val ASSIGNMENT_SUBMIT_SELECTED = "assignment_submit_selected" const val ASSIGNMENT_LAUNCHLTI_SELECTED = "assignment_launchlti_selected" + const val ASSIGNMENT_LIST_SORT_BY_TIME_SELECTED = "assignment_list_sort_by_time_selected" + const val ASSIGNMENT_LIST_SORT_BY_TYPE_SELECTED = "assignment_list_sort_by_type_selected" + const val ASSIGNMENT_SUBMIT_SELECTED = "assignment_submit_selected" const val SUBMIT_FILEUPLOAD_SELECTED = "submit_fileupload_selected" const val SUBMIT_ONLINEURL_SELECTED = "submit_onlineurl_selected" const val SUBMIT_TEXTENTRY_SELECTED = "submit_textentry_selected" 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 591e775bfd..de4d85d4ca 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 @@ -25,6 +25,8 @@ import com.instructure.canvasapi2.models.* import java.util.* import java.util.regex.Pattern +private const val WORKFLOW_STATE_DELETED = "deleted" + fun Assignment.SubmissionType.prettyPrint(context: Context): String = Assignment.submissionTypeToPrettyPrintString(this, context) ?: "" @@ -50,6 +52,7 @@ fun Course.isValidTerm(): Boolean = term?.endDate?.after(Date()) ?: true || hasV fun Course.hasValidSection(): Boolean = sections.any { it.endDate?.after(Date()) ?: false } fun Course.hasActiveEnrollment(): Boolean = enrollments?.any { it.enrollmentState == EnrollmentAPI.STATE_ACTIVE } ?: false fun Course.isInvited(): Boolean = enrollments?.any { it.enrollmentState == EnrollmentAPI.STATE_INVITED } ?: false +fun Course.isNotDeleted(): Boolean = workflowState != WORKFLOW_STATE_DELETED fun MediaComment.asAttachment() = Attachment().also { it.contentType = contentType ?: "" diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/RemoteConfigUtils.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/RemoteConfigUtils.kt index ff7108aa69..b67f5cf155 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/RemoteConfigUtils.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/utils/RemoteConfigUtils.kt @@ -14,8 +14,7 @@ enum class RemoteConfigParam(val rc_name: String, val safeValueAsString: String) TEST_BOOL("test_bool", "false"), TEST_FLOAT("test_float", "0f"), TEST_LONG("test_long", "42"), - TEST_STRING("test_string", "hey there"), - SHOW_TEACHER_SYLLABUS("show_teacher_syllabus", "false") + TEST_STRING("test_string", "hey there") } /** 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 fcc8b5f791..fa74d9c9f9 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 @@ -102,7 +102,7 @@ class CoursesApiPactTests : ApiPactTestBase() { //region Test grabbing all courses // - val allCoursesQuery = "include[]=term&include[]=total_scores&include[]=license&include[]=is_public&include[]=needs_grading_count&include[]=permissions&include[]=favorites&include[]=current_grading_period_scores&include[]=course_image&include[]=sections&state[]=completed&state[]=available" + val allCoursesQuery = "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 allCoursesPath = "/api/v1/courses" val allCoursesFieldInfo = listOf( // Evidently, permissions info is *not* returned from this call, even though include[]=permissions is specified 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 9b62904448..0e32a26251 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 @@ -21,17 +21,16 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.models.Section import com.instructure.canvasapi2.models.Term -import com.instructure.canvasapi2.utils.DateHelper import com.instructure.canvasapi2.utils.Logger +import com.instructure.canvasapi2.utils.isNotDeleted import org.junit.Assert.* import org.junit.Before import org.junit.Test import java.time.OffsetDateTime -import java.util.* class CourseTest { - val baseCourse = Course(accessRestrictedByDate = false, workflowState = "completed") + private val baseCourse = Course(accessRestrictedByDate = false, workflowState = "available") @Before fun setup() { @@ -476,72 +475,94 @@ class CourseTest { } @Test - fun courseIsReadOnly_accessRestrictedByDate() { + fun courseIsBetweenValidDateRange_accessRestrictedByDate() { val course = Course(accessRestrictedByDate = true) - assertFalse(course.isReadOnlyForCurrentDate()) + assertFalse(course.isBetweenValidDateRange()) } @Test - fun courseIsReadOnly_workFlowStateCompleted() { + fun courseIsBetweenValidDateRange_workFlowStateCompleted() { val course = Course(workflowState = "completed") - assertFalse(course.isReadOnlyForCurrentDate()) + assertFalse(course.isBetweenValidDateRange()) } @Test - fun courseIsReadOnly_restrictedToCourseDatesWithInValidDates() { - val startDate = OffsetDateTime.now().minusDays(30) - val endDate = OffsetDateTime.now().minusDays(20) + fun courseIsBetweenValidDateRange_restrictedToCourseDatesWithInValidDates() { + val startDate = OffsetDateTime.now().minusDays(30).withNano(0) + val endDate = OffsetDateTime.now().minusDays(20).withNano(0) val course = baseCourse.copy(restrictEnrollmentsToCourseDate = true, startAt = startDate.toString(), endAt = endDate.toString()) - assertFalse(course.isReadOnlyForCurrentDate()) + assertFalse(course.isBetweenValidDateRange()) } @Test - fun courseIsReadOnly_invalidTermDates() { - val badStartDate = OffsetDateTime.now().minusDays(30) - val badEndDate = OffsetDateTime.now().minusDays(20) + fun `Is between valid date range when enrollment is restricted to course dates and is between course dates`() { + val startDate = OffsetDateTime.now().minusDays(30).withNano(0) + val endDate = OffsetDateTime.now().plusDays(20).withNano(0) + val course = baseCourse.copy(restrictEnrollmentsToCourseDate = true, + startAt = startDate.toString(), endAt = endDate.toString()) + + assertTrue(course.isBetweenValidDateRange()) + } - val startDate = OffsetDateTime.now().minusDays(50) - val endDate = OffsetDateTime.now().plusDays(50) + @Test + fun courseIsBetweenValidDateRange_invalidTermDates() { + val badStartDate = OffsetDateTime.now().minusDays(30).withNano(0) + val badEndDate = OffsetDateTime.now().minusDays(20).withNano(0) val term = Term(startAt = badStartDate.toString(), endAt = badEndDate.toString()) - val course = baseCourse.copy(restrictEnrollmentsToCourseDate = true, - startAt = startDate.toString(), endAt = endDate.toString(), term = term) + val course = baseCourse.copy(restrictEnrollmentsToCourseDate = false, term = term) - assertFalse(course.isReadOnlyForCurrentDate()) + assertFalse(course.isBetweenValidDateRange()) } @Test - fun courseIsReadOnly_invalidSectionDates() { - val badStartDate = OffsetDateTime.now().minusDays(30) - val badEndDate = OffsetDateTime.now().minusDays(20) + fun courseIsBetweenValidDateRange_invalidSectionDates() { + val badStartDate = OffsetDateTime.now().minusDays(30).withNano(0) + val badEndDate = OffsetDateTime.now().minusDays(20).withNano(0) - val startDate = OffsetDateTime.now().minusDays(50) - val endDate = OffsetDateTime.now().plusDays(50) + val startDate = OffsetDateTime.now().minusDays(50).withNano(0) + val endDate = OffsetDateTime.now().plusDays(50).withNano(0) val term = Term(startAt = startDate.toString(), endAt = endDate.toString()) val section = Section(startAt = badStartDate.toString(), endAt = badEndDate.toString()) - val course = baseCourse.copy(restrictEnrollmentsToCourseDate = true, + val course = baseCourse.copy(restrictEnrollmentsToCourseDate = false, startAt = startDate.toString(), endAt = endDate.toString(), term = term, sections = listOf(section)) - assertFalse(course.isReadOnlyForCurrentDate()) + assertFalse(course.isBetweenValidDateRange()) } @Test - fun courseIsReadOnly_validDatesAllTheWayDown() { - val startDate = OffsetDateTime.now().minusDays(50) - val endDate = OffsetDateTime.now().plusDays(50) + fun courseIsBetweenValidDateRange_validDatesAllTheWayDown() { + val startDate = OffsetDateTime.now().minusDays(50).withNano(0) + val endDate = OffsetDateTime.now().plusDays(50).withNano(0) val term = Term(startAt = startDate.toString(), endAt = endDate.toString()) val section = Section(startAt = startDate.toString(), endAt = endDate.toString()) - val course = baseCourse.copy(restrictEnrollmentsToCourseDate = true, + val course = baseCourse.copy(restrictEnrollmentsToCourseDate = false, startAt = startDate.toString(), endAt = endDate.toString(), term = term, sections = listOf(section)) - assertFalse(course.isReadOnlyForCurrentDate()) + assertTrue(course.isBetweenValidDateRange()) } + @Test + fun `Course is not deleted when workflow state is available`() { + val course = baseCourse.copy(workflowState = "available") + assertTrue(course.isNotDeleted()) + } + @Test + fun `Course is not deleted when workflow state is completed`() { + val course = baseCourse.copy(workflowState = "completed") + assertTrue(course.isNotDeleted()) + } + + @Test + fun `Course is deleted when workflow state is deleted`() { + val course = baseCourse.copy(workflowState = "deleted") + assertFalse(course.isNotDeleted()) + } } \ No newline at end of file diff --git a/libs/login-api-2/src/main/res/values/styles.xml b/libs/login-api-2/src/main/res/values/styles.xml index a13ba74329..75e26243e4 100644 --- a/libs/login-api-2/src/main/res/values/styles.xml +++ b/libs/login-api-2/src/main/res/values/styles.xml @@ -21,7 +21,7 @@ diff --git a/libs/pandares/src/main/res/drawable/ic_chevron_down.xml b/libs/pandares/src/main/res/drawable/ic_chevron_down.xml new file mode 100644 index 0000000000..d74ab27fab --- /dev/null +++ b/libs/pandares/src/main/res/drawable/ic_chevron_down.xml @@ -0,0 +1,5 @@ + + + diff --git a/libs/pandares/src/main/res/values/colors.xml b/libs/pandares/src/main/res/values/colors.xml index 92cba4055a..087e0f7ece 100644 --- a/libs/pandares/src/main/res/values/colors.xml +++ b/libs/pandares/src/main/res/values/colors.xml @@ -57,7 +57,7 @@ #F5F5F5 #9D9FA2 #00ac18 - #8B969E + #556572 #50000000 #F5F5F5 #EBEDEE @@ -122,7 +122,7 @@ #008ee2 #0f78db #c7cdd1 - #8b969e + #556572 #2d3b45 #78848c #E0E0E0 @@ -214,7 +214,7 @@ #FAFAFA - #8b969e + #556572 #d8dcdf @@ -243,9 +243,9 @@ #C7CDD1 #EBEDEE #C7CDD1 - #8b969e + #556572 #2d3b45 - #8b969e + #556572 #AA000000 diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index bbc2332993..95697abfc6 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -102,6 +102,7 @@ No Online Submissions This assignment links to an external tool for submissions. Open Tool + Submission text Out of %s points @@ -128,6 +129,7 @@ Attach files to your comment by tapping an option below Have questions about your assignment?\nMessage your instructor. This message could not be sent. Tap to try again. + Media Upload Media Upload - Audio Media Upload - Video Text Submission @@ -197,6 +199,17 @@ No Assignments in this group This assignment is excused and will not be considered in the total calculation EX + Sort by + Time + Type + Sort by Time + Sort by Type + Cancel + All + Sort assignments button, sort by time + Sort assignments button, sort by type + Assignments sorted by time + Assignments sorted by type Points\u0020 @@ -256,6 +269,7 @@ Create Folder Create A File Show Create File and Create Folder Buttons + Hide Create File and Create Folder Buttons An error occurred during folder creation. Rename Rename file @@ -1127,4 +1141,6 @@ Criterion rating %s %s, %s more information + Create File and Folder buttons visible + Create File and Folder buttons invisible diff --git a/libs/pandautils/src/main/assets/discussion_html_header_item.html b/libs/pandautils/src/main/assets/discussion_html_header_item.html index d2b9a0adc2..afed2230ed 100644 --- a/libs/pandautils/src/main/assets/discussion_html_header_item.html +++ b/libs/pandautils/src/main/assets/discussion_html_header_item.html @@ -81,7 +81,7 @@ font-size: 12px; overflow: hidden; white-space: nowrap; - color:#8B969E; + color:#556572; } .timestamp_wrapper { @@ -182,7 +182,7 @@ } .reply, .likes, .delete, .menu { - color: #8B969E; + color: #556572; width: 100%; height: 30px; text-align: center; @@ -196,7 +196,7 @@ } .deleted_info { - color: #8B969E; + color: #556572; width: 100%; height: 30px; height: 30px; @@ -214,7 +214,7 @@ margin-left: 10px; margin-right: 10px; float:left; - color: #8B969E; + color: #556572; height: 30px; font-size: 18px; text-align: center; @@ -290,7 +290,7 @@ width: 4px; height: 4px; margin: 2px; - background: #8B969E; + background: #556572; border-radius: 50%; display: block; } diff --git a/libs/pandautils/src/main/assets/discussion_html_header_item_rtl.html b/libs/pandautils/src/main/assets/discussion_html_header_item_rtl.html index 258317f14d..369aebd874 100644 --- a/libs/pandautils/src/main/assets/discussion_html_header_item_rtl.html +++ b/libs/pandautils/src/main/assets/discussion_html_header_item_rtl.html @@ -82,7 +82,7 @@ font-size: 12px; overflow: hidden; white-space: nowrap; - color:#8B969E; + color:#556572; } .timestamp_wrapper { @@ -183,7 +183,7 @@ } .reply, .likes, .delete, .menu { - color: #8B969E; + color: #556572; width: 100%; height: 30px; text-align: center; @@ -197,7 +197,7 @@ } .deleted_info { - color: #8B969E; + color: #556572; width: 100%; height: 30px; height: 30px; @@ -215,7 +215,7 @@ margin-right: 10px; margin-left: 10px; float:right; - color: #8B969E; + color: #556572; height: 30px; font-size: 18px; text-align: center; @@ -291,7 +291,7 @@ width: 4px; height: 4px; margin: 2px; - background: #8B969E; + background: #556572; border-radius: 50%; display: block; } diff --git a/libs/pandautils/src/main/assets/discussion_html_template_item.html b/libs/pandautils/src/main/assets/discussion_html_template_item.html index 96ebca6723..8029485300 100644 --- a/libs/pandautils/src/main/assets/discussion_html_template_item.html +++ b/libs/pandautils/src/main/assets/discussion_html_template_item.html @@ -191,7 +191,7 @@ .unlikes_count___ENTRY_ID__ { float: right; margin-right: 8px; - color: #8b969e; + color: #556572; } .likes_icon_wrapper___ENTRY_ID__ { diff --git a/libs/pandautils/src/main/assets/discussion_html_template_item_rtl.html b/libs/pandautils/src/main/assets/discussion_html_template_item_rtl.html index 101e0c0392..439c6b15db 100644 --- a/libs/pandautils/src/main/assets/discussion_html_template_item_rtl.html +++ b/libs/pandautils/src/main/assets/discussion_html_template_item_rtl.html @@ -191,7 +191,7 @@ .unlikes_count___ENTRY_ID__ { float: left; margin-left: 8px; - color: #8b969e; + color: #556572; } .likes_icon___ENTRY_ID__ { diff --git a/libs/pandautils/src/main/assets/html_wrapper.html b/libs/pandautils/src/main/assets/html_wrapper.html index da9fd5d0af..2f3b4bc053 100644 --- a/libs/pandautils/src/main/assets/html_wrapper.html +++ b/libs/pandautils/src/main/assets/html_wrapper.html @@ -19,6 +19,7 @@ + {$TITLE$}