From 0b6720adc806c6b14af08e30ca4229b006ca9905 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 1 Apr 2026 15:33:05 +0530 Subject: [PATCH 001/130] Add StudentGrouping EditViewModel and set up its navigation route. --- .../kotlin/world/respect/AppKoinModule.kt | 3 ++ .../world/respect/app/app/AppNavHost.kt | 13 ++++++++ .../respect/shared/navigation/AppRoutes.kt | 5 ++++ .../detail/StudentGroupingDetailViewModel.kt | 4 +++ .../edit/StudentGroupingEditViewModel.kt | 30 +++++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index 9203819a4..6266fb5fb 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -245,6 +245,7 @@ import world.respect.shared.domain.urltonavcommand.ResolveUrlToNavCommandUseCase import world.respect.shared.viewmodel.scanqrcode.ScanQRCodeViewModel import world.respect.shared.domain.navigation.deferreddeeplink.GetDeferredDeepLinkUseCaseAndroid import world.respect.shared.domain.navigation.onappstart.NavigateOnAppStartUseCase +import world.respect.shared.viewmodel.studentgrouping.edit.StudentGroupingEditViewModel const val SHARED_PREF_SETTINGS_NAME = "respect_settings3_" @@ -384,6 +385,8 @@ val appKoinModule = module { viewModelOf(::EnrollmentEditViewModel) viewModelOf(::InviteQrViewModel) viewModelOf(::CreateAccountSetPasswordViewModel) + viewModelOf(::StudentGroupingEditViewModel) + single { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index ad87b7675..55e454059 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -61,6 +61,7 @@ import world.respect.app.view.scanqrcode.ScanQRCodeScreen import world.respect.app.view.schooldirectory.edit.SchoolDirectoryEditScreen import world.respect.app.view.schooldirectory.list.SchoolDirectoryListScreen import world.respect.app.view.settings.SettingsScreenForViewModel +import world.respect.app.view.studentgrouping.edit.StudentGroupingEditScreen import world.respect.app.viewmodel.respectViewModel import world.respect.shared.navigation.AccountList import world.respect.shared.navigation.Acknowledgement @@ -116,6 +117,7 @@ import world.respect.shared.navigation.SchoolDirectoryEdit import world.respect.shared.navigation.SchoolDirectoryList import world.respect.shared.navigation.Settings import world.respect.shared.navigation.SignupScreen +import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.navigation.TermsAndCondition import world.respect.shared.navigation.WaitingForApproval import world.respect.shared.viewmodel.acknowledgement.AcknowledgementViewModel @@ -157,6 +159,7 @@ import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel import world.respect.shared.viewmodel.settings.SettingsViewModel +import world.respect.shared.viewmodel.studentgrouping.edit.StudentGroupingEditViewModel @Composable @@ -290,6 +293,16 @@ fun AppNavHost( ) } + composable { + val viewModel: StudentGroupingEditViewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController + ) + StudentGroupingEditScreen( + viewModel = viewModel + ) + } + composable { val viewModel: EnrollmentListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 2fd39609b..1ce680ed7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -219,6 +219,11 @@ data class ClazzEdit( val guid: String? ) : RespectAppRoute +@Serializable +data class StudentGroupingEdit( + val guid: String? +) : RespectAppRoute + @Serializable object Report : RespectAppRoute diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt new file mode 100644 index 000000000..96c86c6a3 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -0,0 +1,4 @@ +package world.respect.shared.viewmodel.studentgrouping.detail + +class StudentGroupingDetailViewModel { +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt new file mode 100644 index 000000000..405ed9b1a --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -0,0 +1,30 @@ +package world.respect.shared.viewmodel.studentgrouping.edit + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.koin.core.component.KoinScopeComponent +import org.koin.core.scope.Scope +import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.navigation.StudentGroupingEdit +import world.respect.shared.viewmodel.RespectViewModel + +data class StudentGroupingEditUiState( + val str: String="" +) + +class StudentGroupingEditViewModel( + savedStateHandle: SavedStateHandle, + accountManager: RespectAccountManager, +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + override val scope: Scope = accountManager.requireActiveAccountScope() + + private val _uiState = MutableStateFlow(StudentGroupingEditUiState()) + + val uiState = _uiState.asStateFlow() + + private val route: StudentGroupingEdit = savedStateHandle.toRoute() + + +} From 5494b89bcf2a543e7628395a7496a3e9e29a6dd5 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 1 Apr 2026 17:13:59 +0530 Subject: [PATCH 002/130] student grouping UI and logic in class details and edit screens. --- .../view/clazz/detail/ClazzDetailScreen.kt | 67 ++++++++++++++++--- .../composeResources/values/strings.xml | 2 + .../clazz/detail/ClazzDetailViewModel.kt | 16 ++++- .../edit/StudentGroupingEditViewModel.kt | 63 ++++++++++++++++- 4 files changed, 136 insertions(+), 12 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 09f903975..5b9d7bd9c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -1,19 +1,14 @@ package world.respect.app.view.clazz.detail import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.outlined.Cancel -import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material3.ListItem import androidx.compose.material3.Text @@ -39,16 +34,12 @@ import world.respect.shared.generated.resources.add_teacher import world.respect.shared.generated.resources.add_student import world.respect.shared.generated.resources.description import world.respect.shared.generated.resources.pending_requests -import world.respect.shared.generated.resources.accept_invite import world.respect.shared.generated.resources.collapse_pending_invites import world.respect.shared.generated.resources.collapse_students import world.respect.shared.generated.resources.collapse_teachers -import world.respect.shared.generated.resources.date_of_birth -import world.respect.shared.generated.resources.dismiss_invite import world.respect.shared.generated.resources.expand_pending_invites import world.respect.shared.generated.resources.expand_students import world.respect.shared.generated.resources.expand_teachers -import world.respect.shared.generated.resources.gender_literal import world.respect.shared.generated.resources.students import world.respect.shared.generated.resources.teachers import world.respect.shared.util.SortOrderOption @@ -62,6 +53,8 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import world.respect.shared.generated.resources.create_group +import world.respect.shared.generated.resources.groups import world.respect.shared.generated.resources.manage_enrollments import world.respect.shared.generated.resources.more_options import world.respect.shared.generated.resources.remove_from_class @@ -86,6 +79,7 @@ fun ClazzDetailScreen( onClickRemovePersonFromClass = viewModel::onClickRemovePersonFromClass, onClickManageEnrollments = viewModel::onClickManageEnrollments, onClickPerson = viewModel::onClickPerson, + onClickCreateGroup=viewModel::onClickCreateGroup ) } @@ -103,6 +97,7 @@ fun ClazzDetailScreen( onClickRemovePersonFromClass: (Person, EnrollmentRoleEnum) -> Unit, onClickManageEnrollments: (Person, EnrollmentRoleEnum) -> Unit, onClickPerson: (Person) -> Unit, + onClickCreateGroup:()-> Unit ) { val teacherPager = respectRememberPager(uiState.teachers) val studentPager = respectRememberPager(uiState.students) @@ -362,6 +357,60 @@ fun ClazzDetailScreen( ) } } + + if (uiState.showStudentGrouping) { + + item("student_grouping_header") { + ListItem( + modifier = Modifier + .clickable {}, + headlineContent = { + Text( + modifier = Modifier.padding(top = 24.dp), + text = stringResource( + resource = Res.string.groups + ) + ) + }, + + trailingContent = { + Icon( + imageVector = Icons.Outlined.KeyboardArrowDown, + contentDescription = if (uiState.showStudentGrouping) { + stringResource(Res.string.collapse_students) + } else { + stringResource(Res.string.expand_students) + }, + modifier = Modifier.size(24.dp) + .rotate( + if (uiState.showStudentGrouping) 0f else -90f + ) + ) + } + ) + } + + item("student_grouping") { + ListItem( + modifier = Modifier.clickable { + onClickCreateGroup() + }, + leadingContent = { + Icon( + modifier = Modifier.size(40.dp).padding(8.dp), + imageVector = Icons.Filled.Add, + contentDescription = stringResource(resource = Res.string.create_group) + ) + }, + headlineContent = { + Text( + text = + stringResource(resource = Res.string.create_group) + ) + } + ) + } + } } } diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 52b9cb015..07b4cf9e2 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -142,6 +142,8 @@ Students Add Teacher"> Add Student"> + Create Group"> + Groups"> Accept Invite"> Dismiss invite Pending requests"> diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index fb9550376..426045b1c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -55,6 +55,7 @@ import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.writequeue.EnqueueRunPullSyncUseCase +import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.app.appstate.Snack @@ -84,6 +85,9 @@ data class ClazzDetailUiState( val inviteCodePrefix: String? = null, val showAddStudent: Boolean = false, val showAddTeacher: Boolean = false, + + val showStudentGrouping: Boolean = false, + val isStudentGroupingExpanded: Boolean = true, ) class ClazzDetailViewModel( @@ -183,7 +187,9 @@ class ClazzDetailViewModel( prev.copy( showAddStudent = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, showAddTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - ) + showStudentGrouping = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, + + ) } _appUiState.update { @@ -347,6 +353,14 @@ class ClazzDetailViewModel( ) } + fun onClickCreateGroup(){ + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingEdit(guid = route.guid) + ) + ) + } + fun onClickManageEnrollments(person: Person, role: EnrollmentRoleEnum) { _navCommandFlow.tryEmit( NavCommand.Navigate( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 405ed9b1a..b19d7db23 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -4,15 +4,35 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import org.koin.core.component.KoinScopeComponent +import org.koin.core.component.inject import org.koin.core.scope.Scope +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.school.PersonDataSource +import world.respect.datalayer.school.model.EnrollmentRoleEnum +import world.respect.datalayer.school.model.Person +import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory +import world.respect.datalayer.shared.paging.IPagingSourceFactory +import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder +import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.create_group +import world.respect.shared.generated.resources.save import world.respect.shared.navigation.StudentGroupingEdit +import world.respect.shared.resources.UiText +import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState +import kotlin.getValue data class StudentGroupingEditUiState( - val str: String="" -) + val nameError: UiText? = null, + val students: IPagingSourceFactory = EmptyPagingSourceFactory(), + + ) class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, @@ -24,7 +44,46 @@ class StudentGroupingEditViewModel( val uiState = _uiState.asStateFlow() + private val schoolDataSource: SchoolDataSource by inject() + private val route: StudentGroupingEdit = savedStateHandle.toRoute() + private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { + return PagingSourceFactoryHolder { + schoolDataSource.personDataSource.listAsPagingSource( + loadParams = DataLoadParams(), + params = PersonDataSource.GetListParams( + filterByClazzUid = route.guid, + filterByEnrolmentRole = role, + inClassOnDay = localDateInCurrentTimeZone(), + ) + ) + } + } + + private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + + init { + + _appUiState.update { + it.copy( + title = Res.string.create_group.asUiText(), + userAccountIconVisible = false, + hideBottomNavigation = true, + actionBarButtonState = ActionBarButtonUiState( + visible = true, + text = Res.string.save.asUiText(), + onClick = ::onClickSave + ), + ) + } + _uiState.update { + it.copy(students = studentPagingSource) + } + } + + fun onClickSave() { + + } } From c3549242ba3868569a481a173947ed1c8421484d Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 2 Apr 2026 11:40:21 +0530 Subject: [PATCH 003/130] Add student selection logic to `StudentGroupingEditViewModel` and validate student existence before group creation in `ClazzDetailViewModel`. --- .../composeResources/values/strings.xml | 1 + .../clazz/detail/ClazzDetailViewModel.kt | 37 +++++++++++++++---- .../edit/StudentGroupingEditViewModel.kt | 16 +++++++- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 07b4cf9e2..7bb93f6d6 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -142,6 +142,7 @@ Students Add Teacher"> Add Student"> + Add Student to create a Group"> Create Group"> Groups"> Accept Invite"> diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 426045b1c..79404cea0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -55,6 +55,7 @@ import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.writequeue.EnqueueRunPullSyncUseCase +import world.respect.shared.generated.resources.add_student_to_create_group import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState @@ -85,7 +86,6 @@ data class ClazzDetailUiState( val inviteCodePrefix: String? = null, val showAddStudent: Boolean = false, val showAddTeacher: Boolean = false, - val showStudentGrouping: Boolean = false, val isStudentGroupingExpanded: Boolean = true, ) @@ -352,15 +352,36 @@ class ClazzDetailViewModel( ) ) } - - fun onClickCreateGroup(){ - _navCommandFlow.tryEmit( - NavCommand.Navigate( - StudentGroupingEdit(guid = route.guid) - ) - ) + fun onClickCreateGroup() { + viewModelScope.launch { + try { + val students = schoolDataSource.personDataSource.list( + loadParams = DataLoadParams(), + params = PersonDataSource.GetListParams( + filterByClazzUid = route.guid, + filterByEnrolmentRole = EnrollmentRoleEnum.STUDENT, + inClassOnDay = localDateInCurrentTimeZone(), + ) + ).dataOrNull() + + if (students.isNullOrEmpty()) { + snackBarDispatcher.showSnackBar( + Snack(Res.string.add_student_to_create_group.asUiText()) + )} else { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingEdit(guid = route.guid) + ) + ) + } + } catch (e: Throwable) { + Napier.e("onClickCreateGroup ERROR", throwable = e) + snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) + } + } } + fun onClickManageEnrollments(person: Person, role: EnrollmentRoleEnum) { _navCommandFlow.tryEmit( NavCommand.Navigate( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index b19d7db23..eef36a3fc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -31,8 +31,10 @@ import kotlin.getValue data class StudentGroupingEditUiState( val nameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), + val selectedStudentIds: Set = emptySet() - ) + +) class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, @@ -86,4 +88,16 @@ class StudentGroupingEditViewModel( } + fun onStudentCheckedChange(person: Person, isChecked: Boolean) { + _uiState.update { prev -> + val updated = if (isChecked) { + prev.selectedStudentIds + person.guid + } else { + prev.selectedStudentIds - person.guid + } + + prev.copy(selectedStudentIds = updated) + } + } + } From 539311f3707865687ac1dbddd14c0ca270a2fda3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 2 Apr 2026 12:10:36 +0530 Subject: [PATCH 004/130] Remove unused whitespace in StudentGroupingEditViewModel.kt --- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index eef36a3fc..591867d3f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -32,8 +32,6 @@ data class StudentGroupingEditUiState( val nameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), val selectedStudentIds: Set = emptySet() - - ) class StudentGroupingEditViewModel( @@ -95,9 +93,7 @@ class StudentGroupingEditViewModel( } else { prev.selectedStudentIds - person.guid } - prev.copy(selectedStudentIds = updated) } } - } From 4106e0e220941be2913cb96c75c68d5046bcaac5 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 2 Apr 2026 12:54:28 +0530 Subject: [PATCH 005/130] Implement StudentGroupingDetail screen navigation and ViewModel. --- .../kotlin/world/respect/AppKoinModule.kt | 3 +- .../world/respect/app/app/AppNavHost.kt | 13 +++++++ .../respect/shared/navigation/AppRoutes.kt | 3 ++ .../detail/StudentGroupingDetailViewModel.kt | 34 +++++++++++++++++-- .../edit/StudentGroupingEditViewModel.kt | 9 ++++- 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index 6266fb5fb..23e6dc30b 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -245,6 +245,7 @@ import world.respect.shared.domain.urltonavcommand.ResolveUrlToNavCommandUseCase import world.respect.shared.viewmodel.scanqrcode.ScanQRCodeViewModel import world.respect.shared.domain.navigation.deferreddeeplink.GetDeferredDeepLinkUseCaseAndroid import world.respect.shared.domain.navigation.onappstart.NavigateOnAppStartUseCase +import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel import world.respect.shared.viewmodel.studentgrouping.edit.StudentGroupingEditViewModel @@ -386,7 +387,7 @@ val appKoinModule = module { viewModelOf(::InviteQrViewModel) viewModelOf(::CreateAccountSetPasswordViewModel) viewModelOf(::StudentGroupingEditViewModel) - + viewModelOf(::StudentGroupingDetailViewModel) single { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 55e454059..2549352ea 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -61,6 +61,7 @@ import world.respect.app.view.scanqrcode.ScanQRCodeScreen import world.respect.app.view.schooldirectory.edit.SchoolDirectoryEditScreen import world.respect.app.view.schooldirectory.list.SchoolDirectoryListScreen import world.respect.app.view.settings.SettingsScreenForViewModel +import world.respect.app.view.studentgrouping.detail.StudentGroupingDetailScreen import world.respect.app.view.studentgrouping.edit.StudentGroupingEditScreen import world.respect.app.viewmodel.respectViewModel import world.respect.shared.navigation.AccountList @@ -117,6 +118,7 @@ import world.respect.shared.navigation.SchoolDirectoryEdit import world.respect.shared.navigation.SchoolDirectoryList import world.respect.shared.navigation.Settings import world.respect.shared.navigation.SignupScreen +import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.navigation.TermsAndCondition import world.respect.shared.navigation.WaitingForApproval @@ -159,6 +161,7 @@ import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel import world.respect.shared.viewmodel.settings.SettingsViewModel +import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel import world.respect.shared.viewmodel.studentgrouping.edit.StudentGroupingEditViewModel @@ -303,6 +306,16 @@ fun AppNavHost( ) } + composable { + val viewModel: StudentGroupingDetailViewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController + ) + StudentGroupingDetailScreen( + viewModel = viewModel + ) + } + composable { val viewModel: EnrollmentListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 1ce680ed7..43f4d32ad 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -224,6 +224,9 @@ data class StudentGroupingEdit( val guid: String? ) : RespectAppRoute +@Serializable +object StudentGroupingDetail : RespectAppRoute + @Serializable object Report : RespectAppRoute diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 96c86c6a3..b75a05f76 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -1,4 +1,34 @@ package world.respect.shared.viewmodel.studentgrouping.detail -class StudentGroupingDetailViewModel { -} \ No newline at end of file +import androidx.lifecycle.SavedStateHandle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.component.KoinScopeComponent +import org.koin.core.scope.Scope +import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.viewmodel.RespectViewModel + + +data class StudentGroupingDetailUiState( + val str: String = "" +) + +class StudentGroupingDetailViewModel( + savedStateHandle: SavedStateHandle, + accountManager: RespectAccountManager, +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + override val scope: Scope = accountManager.requireActiveAccountScope() + + private val _uiState = MutableStateFlow(StudentGroupingDetailUiState()) + + val uiState = _uiState.asStateFlow() + + init { + _appUiState.update { + it.copy( + //name of the group + ) + } + } +} diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 591867d3f..138f4dc45 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -21,6 +21,9 @@ import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.save +import world.respect.shared.navigation.ClazzDetail +import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText @@ -83,7 +86,11 @@ class StudentGroupingEditViewModel( } fun onClickSave() { - + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingDetail + ) + ) } fun onStudentCheckedChange(person: Person, isChecked: Boolean) { From 94b2f8e54b0ab518002b533c05d96b4ee50089fc Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 2 Apr 2026 15:59:56 +0530 Subject: [PATCH 006/130] Update StudentGrouping viewmodels and add navigation route support. --- .../kotlin/world/respect/shared/navigation/AppRoutes.kt | 2 +- .../studentgrouping/detail/StudentGroupingDetailViewModel.kt | 2 ++ .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 43f4d32ad..a236ca581 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -225,7 +225,7 @@ data class StudentGroupingEdit( ) : RespectAppRoute @Serializable -object StudentGroupingDetail : RespectAppRoute +object StudentGroupingDetail: RespectAppRoute @Serializable object Report : RespectAppRoute diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index b75a05f76..2c1e4a363 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -1,12 +1,14 @@ package world.respect.shared.viewmodel.studentgrouping.detail import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import org.koin.core.component.KoinScopeComponent import org.koin.core.scope.Scope import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.viewmodel.RespectViewModel diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 138f4dc45..e6f3566df 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -18,10 +18,10 @@ import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.save -import world.respect.shared.navigation.ClazzDetail import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.navigation.StudentGroupingEdit From 4105c2546881df3411a26a2340d5a23f3dc782b9 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 8 Apr 2026 17:37:23 +0530 Subject: [PATCH 007/130] Add student grouping detail and edit screens. --- .../detail/StudentGroupingDetailScreen.kt | 43 +++++ .../edit/StudentGroupingEditScreen.kt | 147 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt new file mode 100644 index 000000000..73a63041c --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -0,0 +1,43 @@ +package world.respect.app.view.studentgrouping.detail + +import androidx.compose.foundation.clickable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailUiState +import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel + + +@Composable +fun StudentGroupingDetailScreen( + viewModel: StudentGroupingDetailViewModel +) { + val uiState by viewModel.uiState.collectAsState() + StudentGroupingDetailScreen( + uiState = uiState + ) +} + +@Composable +fun StudentGroupingDetailScreen( + uiState: StudentGroupingDetailUiState +) { + + ListItem( + modifier = Modifier.clickable {}, + leadingContent = { + Icon(Icons.Default.Groups, contentDescription = "") + }, + headlineContent = { + //description of group + }, + supportingContent = { + //number of students in the group + } + ) +} diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt new file mode 100644 index 000000000..1ceabea7d --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -0,0 +1,147 @@ +package world.respect.app.view.studentgrouping.edit + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.paging.compose.collectAsLazyPagingItems +import org.jetbrains.compose.resources.stringResource +import world.respect.app.components.RespectPersonAvatar +import world.respect.app.components.defaultItemPadding +import world.respect.app.components.respectPagingItems +import world.respect.app.components.respectRememberPager +import world.respect.app.components.uiTextStringResource +import world.respect.datalayer.db.school.ext.fullName +import world.respect.datalayer.school.model.EnrollmentRoleEnum +import world.respect.datalayer.school.model.Person +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.description +import world.respect.shared.generated.resources.name +import world.respect.shared.generated.resources.required +import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.studentgrouping.edit.StudentGroupingEditUiState +import world.respect.shared.viewmodel.studentgrouping.edit.StudentGroupingEditViewModel + + +@Composable +fun StudentGroupingEditScreen( + viewModel: StudentGroupingEditViewModel +) { + val uiState by viewModel.uiState.collectAsState() + StudentGroupingEditScreen( + uiState = uiState, + onStudentCheckedChange= viewModel::onStudentCheckedChange, + ) +} + +@Composable +fun StudentGroupingEditScreen( + uiState: StudentGroupingEditUiState, + onStudentCheckedChange : (Person, Boolean) -> Unit, +) { + + val studentPager = respectRememberPager(uiState.students) + val studentLazyPagingItems = studentPager.flow.collectAsLazyPagingItems() + + fun Person?.key(role: EnrollmentRoleEnum, index: Int): Any { + return this?.guid?.let { + Pair(it, role) + } ?: "${role}_$index" + } + + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + + item { + OutlinedTextField( + modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag("name"), + value = "", + onValueChange = { value -> + }, + isError = uiState.nameError != null, + singleLine = true, + label = { + Text(stringResource(Res.string.name) + "*") + }, + supportingText = { + Text( + uiTextStringResource( + uiState.nameError ?: Res.string.required.asUiText() + ) + ) + } + ) + } + + item { + OutlinedTextField( + modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag(("description")), + value = "", + onValueChange = { value -> + }, + label = { + Text( + stringResource(Res.string.description) + ) + }, + ) + } + + respectPagingItems( + items = studentLazyPagingItems, + key = { person, index -> + person?.guid ?: "student_$index" + } + ) { student -> + + val isSelected = student?.guid in uiState.selectedStudentIds + + PersonListItemWithMenu( + person = student, + isSelected = isSelected, + onCheckedChange = { checked -> + student?.let { + onStudentCheckedChange(it, checked) + } + } + ) + } + } +} + +@Composable +fun PersonListItemWithMenu( + person: Person?, + isSelected: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { + onCheckedChange(!isSelected) + }, + leadingContent = { + RespectPersonAvatar(name = person?.fullName() ?: "") + }, + headlineContent = { + Text(text = person?.fullName().orEmpty()) + }, + trailingContent = { + Checkbox( + checked = isSelected, + onCheckedChange = onCheckedChange + ) + } + ) +} \ No newline at end of file From dbcefaaf9c5fee83dc4d871fb383053dd2fa57c5 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 8 Apr 2026 17:41:08 +0530 Subject: [PATCH 008/130] Add TODO comment for xAPI compatibility in StudentGroupingEditViewModel. --- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index e6f3566df..29ecd26fc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -93,6 +93,7 @@ class StudentGroupingEditViewModel( ) } + //this needs to be changed according to xapi fun onStudentCheckedChange(person: Person, isChecked: Boolean) { _uiState.update { prev -> val updated = if (isChecked) { From d326c3ea6da76c02cc08e20c74ea7fc98517399a Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 9 Apr 2026 11:35:09 +0530 Subject: [PATCH 009/130] Add TODO comment for xAPI compatibility in StudentGroupingEditViewModel. --- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 29ecd26fc..69364654c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -18,7 +18,6 @@ import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.save From 1da086133c045c8292e6e235b8c3627b41e4aa08 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 14 Apr 2026 17:26:26 +0530 Subject: [PATCH 010/130] Update StudentGroupingEditViewModel to track selected student names and map them to XapiAgent objects on save. --- .../edit/StudentGroupingEditViewModel.kt | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 69364654c..49ca886c0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -10,9 +10,13 @@ import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.xapi.model.XapiAccount +import world.respect.datalayer.school.xapi.model.XapiAgent +import world.respect.datalayer.school.xapi.model.XapiObjectType import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder @@ -33,12 +37,13 @@ import kotlin.getValue data class StudentGroupingEditUiState( val nameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val selectedStudentIds: Set = emptySet() + val selectedStudentIds: List = emptyList(), + val selectedStudentNames: List = emptyList() ) class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, - accountManager: RespectAccountManager, + var accountManager: RespectAccountManager, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = accountManager.requireActiveAccountScope() @@ -63,7 +68,7 @@ class StudentGroupingEditViewModel( } } - private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) init { @@ -85,6 +90,19 @@ class StudentGroupingEditViewModel( } fun onClickSave() { + val schoolSelfUrl = accountManager.activeAccount?.school?.self + val members = _uiState.value.selectedStudentNames.map { studentName -> + XapiAgent( + name = studentName, + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = studentName, + homePage = schoolSelfUrl.toString() + ) + ) + } + + _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingDetail @@ -92,15 +110,25 @@ class StudentGroupingEditViewModel( ) } - //this needs to be changed according to xapi fun onStudentCheckedChange(person: Person, isChecked: Boolean) { _uiState.update { prev -> - val updated = if (isChecked) { + + val updatedIds = if (isChecked) { prev.selectedStudentIds + person.guid } else { prev.selectedStudentIds - person.guid } - prev.copy(selectedStudentIds = updated) + + val updatedNames = if (isChecked) { + prev.selectedStudentNames + person.fullName() + } else { + prev.selectedStudentNames - person.fullName() + } + + prev.copy( + selectedStudentIds = updatedIds, + selectedStudentNames = updatedNames + ) } } } From 97ffed20c1bcac85947f6bcb50a64c308247af65 Mon Sep 17 00:00:00 2001 From: pooja Date: Wed, 15 Apr 2026 14:26:32 +0400 Subject: [PATCH 011/130] test: add Maestro flow for adding groups to a class --- .../004_001_add_groups_to_a_class_test.yaml | 87 +++++++++++++++++++ .maestro/flows/subflows/admin_add_app.yaml | 2 +- .../admin_add_class_with_members.yaml | 50 +++++++++++ .../admin_add_new_student_to_class.yaml | 19 ++++ .../admin_add_new_teacher_to_class.yaml | 23 +++++ .../flows/subflows/admin_add_student.yaml | 66 -------------- .../create_account_for_class_member.yaml | 20 +++++ 7 files changed, 200 insertions(+), 67 deletions(-) create mode 100644 .maestro/flows/004_001_add_groups_to_a_class_test.yaml create mode 100644 .maestro/flows/subflows/admin_add_class_with_members.yaml create mode 100644 .maestro/flows/subflows/admin_add_new_student_to_class.yaml create mode 100644 .maestro/flows/subflows/admin_add_new_teacher_to_class.yaml delete mode 100644 .maestro/flows/subflows/admin_add_student.yaml create mode 100644 .maestro/flows/subflows/create_account_for_class_member.yaml diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml new file mode 100644 index 000000000..b751e4f6e --- /dev/null +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -0,0 +1,87 @@ +appId: world.respect.app +onFlowStart: + - clearState: world.respect.app + - runScript: + file: "scripts/school_init.js" + env: + TESTCONTROLLER_URL: ${TESTCONTROLLER_URL} + SCHOOL_ADMIN_PASSWORD: ${SCHOOL_ADMIN_PASSWORD} + DIR_ADMIN_AUTH_HEADER: ${DIR_ADMIN_AUTH_HEADER} + SCHOOL_URL: ${SCHOOL_URL} + SCHOOL_NAME: ${SCHOOL_NAME} + URL_SUBSTITUTION: ${URL_SUBSTITUTION} + NAME: "004_001_add_groups_to_a_class_test" + +onFlowComplete: + - runScript: + file: "scripts/teardown.js" +--- +- runFlow: "subflows/school_admin_login_flow.yaml" +- runFlow: "subflows/admin_add_app.yaml" +- runFlow: "subflows/admin_add_class_with_members.yaml" + +- clearState: world.respect.app +- launchApp: + arguments: + respect_directory: ${output.SCHOOL_URL} +- tapOn: "Get Started" +- runFlow: + file: "subflows/get_started_select_school_by_name.yaml" + env: + SCHOOL_NAME: ${SCHOOL_NAME} +- tapOn: + id: "username" +- inputText: "teacherauser" +- tapOn: + id : "password" +- inputText: "test1234" +- tapOn: "Login" +- runFlow: + when: + visible: "Save password for Respect?" + file: "subflows/save_password_prompt_cancel.yaml" +- assertVisible: "Apps" +- tapOn: "Classes" +- assertVisible: + id: "app_title" + text: "Classes" +- assertVisible: "New Class" +- tapOn: "New Class" +- assertVisible: + id: "app_title" + text: "New Class" +- scrollUntilVisible: + element: "Groups" + direction: DOWN +- tapOn: "Create group" +- assertVisible: + id: "app_title" + text: "Create group" +- tapOn: "Name*" +- inputText: "Test Group" +- tapOn: "Description" +- inputText: "This test group functionality" +- tapOn: "StudentA USER" +- tapOn: "StudentC USER" +- tapOn: "Save" +- assertVisible: + id: "app_title" + text: "Test Group" +- assertVisible: "This test group functionality" +- assertVisible: "StudentA USER" +- assertVisible: "StudentC USER" +- assertVisible: + id: "delete_group_btn" +- tapOn: "floating_action_button" #Edit Button +- tapOn: "StudentD USER" +- tapOn: "Save" +- assertVisible: + id: "app_title" + text: "Test Group" +- assertVisible: "StudentD USER" +- back +- assertVisible: + id: "app_title" + text: "New Class" +- scrollUntilVisible: + element: "Test Group" \ No newline at end of file diff --git a/.maestro/flows/subflows/admin_add_app.yaml b/.maestro/flows/subflows/admin_add_app.yaml index e5a4be4be..b5dee3870 100644 --- a/.maestro/flows/subflows/admin_add_app.yaml +++ b/.maestro/flows/subflows/admin_add_app.yaml @@ -8,7 +8,7 @@ appId: world.respect.app id: "floating_action_button" - tapOn: "Add from Link" - tapOn: "Link*" -- inputText: "https://respect.world/respect-ds/case_valid/appmanifest.json" +- inputText: ${output.SCHOOL_URL}static-resources/respect-ds/case_valid/appmanifest.json - tapOn: "Next" - assertVisible: id: "app_title" diff --git a/.maestro/flows/subflows/admin_add_class_with_members.yaml b/.maestro/flows/subflows/admin_add_class_with_members.yaml new file mode 100644 index 000000000..18dd30ff9 --- /dev/null +++ b/.maestro/flows/subflows/admin_add_class_with_members.yaml @@ -0,0 +1,50 @@ +appId: world.respect.app + +--- +# Admin add new class +- tapOn: "Classes" +- assertVisible: + id: "app_title" + text: "Classes" +- tapOn: + id: "floating_action_button" # +Class button +- assertVisible: + id: "app_title" + text: "Add class" +- tapOn: "Class name*" +- inputText: "New Class" +- tapOn: "Save" + +- runFlow: + file: "admin_add_new_student_to_class.yaml" + env: + FIRSTNAME: StudentA + LASTNAME: User +- runFlow: + file: "admin_add_new_student_to_class.yaml" + env: + FIRSTNAME: StudentB + LASTNAME: User +- runFlow: + file: "admin_add_new_student_to_class.yaml" + env: + FIRSTNAME: StudentC + LASTNAME: User +- runFlow: + file: "admin_add_new_student_to_class.yaml" + env: + FIRSTNAME: StudentD + LASTNAME: User + +- runFlow: + file: "admin_add_new_teacher_to_class.yaml" + env: + FIRSTNAME: TeacherA + LASTNAME: User + +- runFlow: + file: "create_account_for_class_member.yaml" + env: + CLASSNAME: "New Class" + MEMBERNAME: TeacherA User + MEMBERUSERNAME: teacherauser diff --git a/.maestro/flows/subflows/admin_add_new_student_to_class.yaml b/.maestro/flows/subflows/admin_add_new_student_to_class.yaml new file mode 100644 index 000000000..06afddd55 --- /dev/null +++ b/.maestro/flows/subflows/admin_add_new_student_to_class.yaml @@ -0,0 +1,19 @@ +appId: world.respect.app + +--- +- tapOn: "Classes" +- tapOn: "New Class" +- tapOn: "Add Student" +- tapOn: "Add person" +- assertVisible: + id: "app_title" + text: "Add person" +- tapOn: "First names*" +- inputText: ${FIRSTNAME} +- tapOn: "Last name*" +- inputText: ${LASTNAME} +- tapOn: "Gender*" +- tapOn: "Female" +- tapOn: "Save" +- assertVisible: + text: ${FIRSTNAME} ${LASTNAME} diff --git a/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml b/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml new file mode 100644 index 000000000..9c324dcf3 --- /dev/null +++ b/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml @@ -0,0 +1,23 @@ +appId: world.respect.app + +--- +- tapOn: "Classes" +- tapOn: "New Class" +- tapOn: "Add Teacher" +- tapOn: "Add person" +- assertVisible: + id: "app_title" + text: "Add person" +- tapOn: "First names*" +- inputText: ${FIRSTNAME} +- tapOn: "Last name*" +- inputText: ${LASTNAME} +- tapOn: "Gender*" +- tapOn: "Female" +- tapOn: + id: "role" +- tapOn: + text: "Teacher" +- tapOn: "Save" +- assertVisible: + text: ${FIRSTNAME} ${LASTNAME} diff --git a/.maestro/flows/subflows/admin_add_student.yaml b/.maestro/flows/subflows/admin_add_student.yaml deleted file mode 100644 index 1d8c4776c..000000000 --- a/.maestro/flows/subflows/admin_add_student.yaml +++ /dev/null @@ -1,66 +0,0 @@ -appId: world.respect.app - ---- -- tapOn: "Classes" -- tapOn: "New Class" -# Admin copy invite code to add student -- tapOn: "Add Student" -- copyTextFrom: - id: "invite_code" - - -# Student sign-up via invite code to the class -- clearState: world.respect.app -- launchApp: - arguments: - respect_directory: ${output.SCHOOL_URL} - -- tapOn: "Get Started" - -- runFlow: - file: "get_started_select_school_by_name.yaml" - env: - SCHOOL_NAME: ${SCHOOL_NAME} -- tapOn: "I have an invite code" -- tapOn: "Invite code" -- pasteText -- tapOn: "Next" -- assertVisible: - id: "app_title" -- tapOn: "I’m a Student" -- assertVisible: - id: "app_title" - text: "Your profile" -- tapOn: "Your name*" -- inputText: "Student User" -- tapOn: "Gender*" -- tapOn: "Female" -- tapOn: "Your date of birth*" -- runScript: - file: "../scripts/setDate.js" -- inputText: ${output.pastYearDateC} -- tapOn: "Next" -- assertVisible: - id: "app_title" - text: "Create account" -- assertVisible: "studentuser" #username -- tapOn: "Next" -- tapOn: "Password*" -- inputText: "tests1" -- tapOn: "Sign-up" -- assertVisible: - id: "app_title" - text: "Waiting for approval" -- assertVisible: "Please wait" - -# Admin approve student's request to join the class -- clearState: world.respect.app -- runFlow: "school_admin_login_flow.yaml" -- tapOn: "Classes" -- assertVisible: - id: "app_title" - text: "Classes" -- tapOn: "New Class" -- assertVisible: "Pending requests.*" -- assertVisible: "Student User.*" -- tapOn: "Accept Invite" \ No newline at end of file diff --git a/.maestro/flows/subflows/create_account_for_class_member.yaml b/.maestro/flows/subflows/create_account_for_class_member.yaml new file mode 100644 index 000000000..07f367c0c --- /dev/null +++ b/.maestro/flows/subflows/create_account_for_class_member.yaml @@ -0,0 +1,20 @@ +appId: world.respect.app + +--- + +- tapOn: "Classes" +- assertVisible: + id: "app_title" + text: "Classes" +- tapOn: ${CLASSNAME} +- tapOn: ${MEMBERNAME} +- tapOn: "Create account" +- assertVisible: + id: "app_title" + text: "Create account" +- assertVisible: + text: ${MEMBERUSERNAME} +- tapOn: "Password" +- inputText: "test1234" +- tapOn: "Save" +- assertVisible: "Manage account" \ No newline at end of file From ebf112716c881c460bbb513e75de6f5e4a4f8ad9 Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 20 Apr 2026 12:12:10 +0400 Subject: [PATCH 012/130] Move `004_001_add_groups_to_a_class_test.yaml` Maestro flow to `pending_flows`. --- .../004_001_add_groups_to_a_class_test.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .maestro/{flows => pending_flows}/004_001_add_groups_to_a_class_test.yaml (100%) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/pending_flows/004_001_add_groups_to_a_class_test.yaml similarity index 100% rename from .maestro/flows/004_001_add_groups_to_a_class_test.yaml rename to .maestro/pending_flows/004_001_add_groups_to_a_class_test.yaml From 6c0cebb96bedb5f90e3dca5e4bd9f5458f7d3885 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 22 Apr 2026 09:10:00 +0530 Subject: [PATCH 013/130] Update --- .../004_001_add_groups_to_a_class_test.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .maestro/{pending_flows => flows}/004_001_add_groups_to_a_class_test.yaml (100%) diff --git a/.maestro/pending_flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml similarity index 100% rename from .maestro/pending_flows/004_001_add_groups_to_a_class_test.yaml rename to .maestro/flows/004_001_add_groups_to_a_class_test.yaml From 30ed5cb4935d617f6bac4fa59ffc288c19079f79 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 22 Apr 2026 14:41:20 +0530 Subject: [PATCH 014/130] Implement xAPI statement logging for student group creation in `StudentGroupingEditViewModel`. --- .../edit/StudentGroupingEditViewModel.kt | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 49ca886c0..83349a216 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -1,10 +1,12 @@ package world.respect.shared.viewmodel.studentgrouping.edit import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope @@ -14,14 +16,22 @@ import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.xapi.model.VERB_COMPLETED import world.respect.datalayer.school.xapi.model.XapiAccount +import world.respect.datalayer.school.xapi.model.XapiActor import world.respect.datalayer.school.xapi.model.XapiAgent +import world.respect.datalayer.school.xapi.model.XapiGroup import world.respect.datalayer.school.xapi.model.XapiObjectType +import world.respect.datalayer.school.xapi.model.XapiState +import world.respect.datalayer.school.xapi.model.XapiStatement +import world.respect.datalayer.school.xapi.model.XapiVerb import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder +import world.respect.lib.serializers.InstantAsISO8601 import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.account.RespectSessionAndPerson import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.save @@ -32,9 +42,14 @@ import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState +import java.util.UUID import kotlin.getValue +import kotlin.time.Clock +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid data class StudentGroupingEditUiState( + val selectedAccount: RespectSessionAndPerson? = null, val nameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), val selectedStudentIds: List = emptyList(), @@ -43,9 +58,9 @@ data class StudentGroupingEditUiState( class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, - var accountManager: RespectAccountManager, + var respectAccountManager: RespectAccountManager, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { - override val scope: Scope = accountManager.requireActiveAccountScope() + override val scope: Scope = respectAccountManager.requireActiveAccountScope() private val _uiState = MutableStateFlow(StudentGroupingEditUiState()) @@ -89,8 +104,17 @@ class StudentGroupingEditViewModel( } } + @OptIn(ExperimentalUuidApi::class) fun onClickSave() { - val schoolSelfUrl = accountManager.activeAccount?.school?.self + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + val groupName ="" + viewModelScope.launch { + respectAccountManager.selectedAccountAndPersonFlow.collect { accountAndPerson -> + _uiState.update { prev -> + prev.copy(selectedAccount = accountAndPerson) + } + } + } val members = _uiState.value.selectedStudentNames.map { studentName -> XapiAgent( name = studentName, @@ -103,6 +127,36 @@ class StudentGroupingEditViewModel( } + val group = XapiGroup( + objectType = XapiObjectType.Group, + account = XapiAccount( + name = groupName, + homePage = schoolSelfUrl.toString() + ), + member = members + ) + + val actor = XapiAgent( + name = _uiState.value.selectedAccount?.person?.fullName(), + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = _uiState.value.selectedAccount?.person?.fullName()?:"", + homePage = schoolSelfUrl.toString() + ) + ) + + val verb = XapiVerb( + id = VERB_COMPLETED + ) + + val statement= XapiStatement( + actor = actor, + verb = verb, + `object` = group + ) + launchWithLoadingIndicator { + schoolDataSource.xapiStatementDataSource.store(listOf(statement)) + } _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingDetail From 2366df0ff0ede0fa21b0681f2932ccb946b6fbc9 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Sun, 26 Apr 2026 13:44:07 +0530 Subject: [PATCH 015/130] Implement student grouping creation logic in `StudentGroupingEditViewModel`. * Update `StudentGroupingEditScreen` to handle group name changes and display validation errors. * Extend `Clazz` model and database entities to support a `metadata` field for storing group IDs. * Implement `onClickSave` in `StudentGroupingEditViewModel` to generate xAPI statements for group creation and update class metadata. * Add `VERB_CREATED` and `VERB_UPDATED` constants to `XapiVerb`. * Update navigation and UI padding for the student grouping entry point. --- .../view/clazz/detail/ClazzDetailScreen.kt | 2 +- .../edit/StudentGroupingEditScreen.kt | 30 +-- .../db/school/adapters/ClassAdapter.kt | 2 + .../db/school/entities/ClassEntity.kt | 2 + .../respect/datalayer/school/model/Clazz.kt | 4 + .../datalayer/school/xapi/model/XapiVerb.kt | 4 + .../respect/shared/navigation/AppRoutes.kt | 2 +- .../clazz/detail/ClazzDetailViewModel.kt | 2 +- .../edit/StudentGroupingEditViewModel.kt | 193 ++++++++++++------ 9 files changed, 160 insertions(+), 81 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 5b9d7bd9c..5ab2c7fbf 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -392,7 +392,7 @@ fun ClazzDetailScreen( item("student_grouping") { ListItem( - modifier = Modifier.clickable { + modifier = Modifier.padding(bottom = 40.dp).clickable { onClickCreateGroup() }, leadingContent = { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 1ceabea7d..2a8840b2c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -39,15 +39,18 @@ fun StudentGroupingEditScreen( val uiState by viewModel.uiState.collectAsState() StudentGroupingEditScreen( uiState = uiState, - onStudentCheckedChange= viewModel::onStudentCheckedChange, + onStudentCheckedChange = viewModel::onStudentCheckedChange, + onGroupNameChanged = viewModel::onGroupNameChanged ) } @Composable fun StudentGroupingEditScreen( uiState: StudentGroupingEditUiState, - onStudentCheckedChange : (Person, Boolean) -> Unit, -) { + onStudentCheckedChange: (Person, Boolean) -> Unit, + onGroupNameChanged: (String) -> Unit, + + ) { val studentPager = respectRememberPager(uiState.students) val studentLazyPagingItems = studentPager.flow.collectAsLazyPagingItems() @@ -65,10 +68,11 @@ fun StudentGroupingEditScreen( item { OutlinedTextField( modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag("name"), - value = "", + value = uiState.groupName, onValueChange = { value -> + onGroupNameChanged(value) }, - isError = uiState.nameError != null, + isError = uiState.groupNameError != null, singleLine = true, label = { Text(stringResource(Res.string.name) + "*") @@ -76,27 +80,13 @@ fun StudentGroupingEditScreen( supportingText = { Text( uiTextStringResource( - uiState.nameError ?: Res.string.required.asUiText() + uiState.groupNameError ?: Res.string.required.asUiText() ) ) } ) } - item { - OutlinedTextField( - modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag(("description")), - value = "", - onValueChange = { value -> - }, - label = { - Text( - stringResource(Res.string.description) - ) - }, - ) - } - respectPagingItems( items = studentLazyPagingItems, key = { person, index -> diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt index cfb7d9dcc..1c9701b3b 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt @@ -22,6 +22,7 @@ fun ClassEntities.toModel(): Clazz { guid = clazz.cGuid, title = clazz.cTitle, status = clazz.cStatus, + metadata = clazz.cMetadata, description = clazz.cDescription, lastModified = clazz.cLastModified, stored = clazz.cStored, @@ -54,6 +55,7 @@ fun Clazz.toEntities( cGuidHash = classUidNum, cTitle = title, cStatus = status, + cMetadata=metadata, cDescription = description, cLastModified = lastModified, cStored = stored, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt index 891a4efbf..beac03d48 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt @@ -2,6 +2,7 @@ package world.respect.datalayer.db.school.entities import androidx.room.Entity import androidx.room.PrimaryKey +import kotlinx.serialization.json.JsonObject import world.respect.datalayer.school.model.StatusEnum import kotlin.time.Instant @@ -12,6 +13,7 @@ data class ClassEntity( val cGuidHash: Long, val cTitle: String, val cStatus: StatusEnum, + val cMetadata: JsonObject?, val cDescription: String?, val cLastModified: Instant, val cStored: Instant, diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt index af5be7a92..8e6e8b972 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt @@ -1,6 +1,7 @@ package world.respect.datalayer.school.model import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject import world.respect.datalayer.shared.ModelWithTimes import world.respect.lib.serializers.InstantAsISO8601 import kotlin.random.Random @@ -12,6 +13,7 @@ data class Clazz( val title: String, val status: StatusEnum = StatusEnum.ACTIVE, val description: String? = null, + val metadata: JsonObject? = null, override val lastModified: InstantAsISO8601 = Clock.System.now(), override val stored: InstantAsISO8601 = Clock.System.now(), val teacherInviteGuid: String? = null, @@ -36,6 +38,8 @@ data class Clazz( const val DEFAULT_INVITE_CODE_MAX = 100_000 const val DEFAULT_INVITE_CODE_LEN = 6 + + const val GROUP_IDS = "groupIds" } } diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt index 935a0411e..e06d10b81 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt @@ -7,6 +7,10 @@ const val VERB_COMPLETED = "http://adlnet.gov/expapi/verbs/completed" const val VERB_PROGRESSED = "http://adlnet.gov/expapi/verbs/progressed" +const val VERB_CREATED = "http://adlnet.gov/expapi/verbs/create" + +const val VERB_UPDATED = "http://adlnet.gov/expapi/verbs/update" + @Serializable data class XapiVerb( val id: String, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index a236ca581..30cd88773 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -221,7 +221,7 @@ data class ClazzEdit( @Serializable data class StudentGroupingEdit( - val guid: String? + val guid: String ) : RespectAppRoute @Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 79404cea0..6d31669f3 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -326,7 +326,7 @@ class ClazzDetailViewModel( it.copy( lastModified = modTime, status = if(it.beginDate == today) { - StatusEnum.TO_BE_DELETED //probably was just added by mistake + StatusEnum.TO_BE_DELETED }else { it.status }, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 83349a216..7b8e59e5b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -3,37 +3,46 @@ package world.respect.shared.viewmodel.studentgrouping.edit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.putJsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.DataLoadingState import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.db.school.ext.fullName +import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource +import world.respect.datalayer.school.model.Clazz +import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person -import world.respect.datalayer.school.xapi.model.VERB_COMPLETED +import world.respect.datalayer.school.xapi.model.VERB_CREATED import world.respect.datalayer.school.xapi.model.XapiAccount -import world.respect.datalayer.school.xapi.model.XapiActor import world.respect.datalayer.school.xapi.model.XapiAgent import world.respect.datalayer.school.xapi.model.XapiGroup import world.respect.datalayer.school.xapi.model.XapiObjectType -import world.respect.datalayer.school.xapi.model.XapiState import world.respect.datalayer.school.xapi.model.XapiStatement import world.respect.datalayer.school.xapi.model.XapiVerb import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder -import world.respect.lib.serializers.InstantAsISO8601 import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.account.RespectSessionAndPerson import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group +import world.respect.shared.generated.resources.required_field import world.respect.shared.generated.resources.save import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.StudentGroupingDetail @@ -42,19 +51,23 @@ import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState -import java.util.UUID import kotlin.getValue import kotlin.time.Clock +import kotlin.toString import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid data class StudentGroupingEditUiState( val selectedAccount: RespectSessionAndPerson? = null, - val nameError: UiText? = null, + val groupName: String = "", + val groupNameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), val selectedStudentIds: List = emptyList(), - val selectedStudentNames: List = emptyList() -) + val selectedStudentNames: List = emptyList(), + val personName: String = "", + val clazz: DataLoadState = DataLoadingState(), + + ) class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, @@ -86,7 +99,6 @@ class StudentGroupingEditViewModel( private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) init { - _appUiState.update { it.copy( title = Res.string.create_group.asUiText(), @@ -102,66 +114,121 @@ class StudentGroupingEditViewModel( _uiState.update { it.copy(students = studentPagingSource) } + + viewModelScope.launch { + respectAccountManager.selectedAccountAndPersonFlow.collect { selectedAccount -> + _uiState.update { + it.copy( + personName = selectedAccount?.person?.fullName() ?: "" + ) + } + } + } + + viewModelScope.launch { + schoolDataSource.classDataSource.findByGuidAsFlow(route.guid).collect { clazz -> + _uiState.update { it.copy(clazz = clazz) } + } + } } @OptIn(ExperimentalUuidApi::class) fun onClickSave() { val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self - val groupName ="" - viewModelScope.launch { - respectAccountManager.selectedAccountAndPersonFlow.collect { accountAndPerson -> - _uiState.update { prev -> - prev.copy(selectedAccount = accountAndPerson) - } + val groupName = _uiState.value.groupName + + if (groupName.isBlank()) { + _uiState.update { prev -> + prev.copy(groupNameError = Res.string.required_field.asUiText()) } + return + } else { + _uiState.update { prev -> prev.copy(groupNameError = null) } } - val members = _uiState.value.selectedStudentNames.map { studentName -> - XapiAgent( - name = studentName, - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = studentName, - homePage = schoolSelfUrl.toString() + + viewModelScope.launch { + try { + val members = _uiState.value.selectedStudentNames.map { studentName -> + XapiAgent( + name = studentName, + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = studentName, + homePage = schoolSelfUrl.toString() + ) + ) + } + + val groupId = Uuid.random().toString() + val group = XapiGroup( + objectType = XapiObjectType.Group, + name = groupName, + account = XapiAccount( + name = groupId, + homePage = schoolSelfUrl.toString() + ), + member = members ) - ) - } + val actor = XapiAgent( + name = _uiState.value.personName, + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = respectAccountManager.activeAccount?.userGuid ?: "", + homePage = schoolSelfUrl.toString() + ) + ) - val group = XapiGroup( - objectType = XapiObjectType.Group, - account = XapiAccount( - name = groupName, - homePage = schoolSelfUrl.toString() - ), - member = members - ) - - val actor = XapiAgent( - name = _uiState.value.selectedAccount?.person?.fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = _uiState.value.selectedAccount?.person?.fullName()?:"", - homePage = schoolSelfUrl.toString() - ) - ) - - val verb = XapiVerb( - id = VERB_COMPLETED - ) - - val statement= XapiStatement( - actor = actor, - verb = verb, - `object` = group - ) - launchWithLoadingIndicator { - schoolDataSource.xapiStatementDataSource.store(listOf(statement)) + val verb = XapiVerb( + id = VERB_CREATED, + display = mapOf("en" to VERB_CREATED) + ) + + val statement = XapiStatement( + actor = actor, + verb = verb, + `object` = group, + timestamp = Clock.System.now() + ) + + schoolDataSource.xapiStatementDataSource.store(listOf(statement)) + + val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch + + val existingGroupIds = clazz.metadata + ?.get(GROUP_IDS) + ?.jsonArray + ?.map { it.jsonPrimitive.content } + ?.toMutableList() + ?: mutableListOf() + + if (!existingGroupIds.contains(groupId)) { + existingGroupIds.add(groupId) + } + + val updatedMetadata = buildJsonObject { + clazz.metadata?.forEach { (key, value) -> + if (key != GROUP_IDS) { + put(key, value) + } + } + putJsonArray(GROUP_IDS) { + existingGroupIds.forEach { add(it) } + } + } + + val updatedClazz = clazz.copy( + metadata = updatedMetadata, + lastModified = Clock.System.now() + ) + + schoolDataSource.classDataSource.store(listOf(updatedClazz)) + + _navCommandFlow.tryEmit(NavCommand.Navigate(StudentGroupingDetail)) + } catch (e: Throwable) { + Napier.e("onClickSave ERROR", throwable = e) + } } - _navCommandFlow.tryEmit( - NavCommand.Navigate( - StudentGroupingDetail - ) - ) } fun onStudentCheckedChange(person: Person, isChecked: Boolean) { @@ -185,4 +252,14 @@ class StudentGroupingEditViewModel( ) } } + + fun onGroupNameChanged(name: String) { + _uiState.update { prev -> + prev.copy( + groupName = name, + groupNameError = if (name.isNotBlank() && prev.groupNameError != null) null else prev.groupNameError + ) + } + } } + From 8b67df179867be2649f9518c6ad63a722e48b09b Mon Sep 17 00:00:00 2001 From: mandviverma Date: Sun, 26 Apr 2026 14:18:29 +0530 Subject: [PATCH 016/130] Update StudentGroupingDetail route to include groupId and initialize StudentGroupingDetailViewModel UI state. --- .../respect/shared/navigation/AppRoutes.kt | 4 ++- .../detail/StudentGroupingDetailViewModel.kt | 25 ++++++++++++++++--- .../edit/StudentGroupingEditViewModel.kt | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 30cd88773..232224166 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -225,7 +225,9 @@ data class StudentGroupingEdit( ) : RespectAppRoute @Serializable -object StudentGroupingDetail: RespectAppRoute +data class StudentGroupingDetail( + val groupId: String +): RespectAppRoute @Serializable object Report : RespectAppRoute diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 2c1e4a363..c63b99161 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -1,15 +1,26 @@ package world.respect.shared.viewmodel.studentgrouping.detail import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent +import org.koin.core.component.inject import org.koin.core.scope.Scope +import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.ext.dataOrNull import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.create_group import world.respect.shared.navigation.StudentGroupingDetail +import world.respect.shared.navigation.StudentGroupingEdit +import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel +import kotlin.getValue data class StudentGroupingDetailUiState( @@ -18,19 +29,27 @@ data class StudentGroupingDetailUiState( class StudentGroupingDetailViewModel( savedStateHandle: SavedStateHandle, - accountManager: RespectAccountManager, + respectAccountManager: RespectAccountManager, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { - override val scope: Scope = accountManager.requireActiveAccountScope() + + override val scope: Scope = respectAccountManager.requireActiveAccountScope() private val _uiState = MutableStateFlow(StudentGroupingDetailUiState()) val uiState = _uiState.asStateFlow() + private val schoolDataSource: SchoolDataSource by inject() + + private val route: StudentGroupingDetail = savedStateHandle.toRoute() + init { _appUiState.update { it.copy( - //name of the group + title = Res.string.create_group.asUiText(), + userAccountIconVisible = false, + hideBottomNavigation = true, ) } } } + diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 7b8e59e5b..2dd976b5b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -224,7 +224,7 @@ class StudentGroupingEditViewModel( schoolDataSource.classDataSource.store(listOf(updatedClazz)) - _navCommandFlow.tryEmit(NavCommand.Navigate(StudentGroupingDetail)) + _navCommandFlow.tryEmit(NavCommand.Navigate(StudentGroupingDetail(groupId))) } catch (e: Throwable) { Napier.e("onClickSave ERROR", throwable = e) } From c761cf4095cd3f326241e9a6cec87a548cc5fd43 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Sun, 26 Apr 2026 22:20:15 +0530 Subject: [PATCH 017/130] Implement student grouping features in class and group detail screens. * Add `getGroupDetail` and `getGroupsByIds` to `XapiActorDataSource` and its database implementation. * Update `ClazzDetailViewModel` to extract and load group details from class metadata. * Enhance `ClazzDetailScreen` to display a list of student groups with toggle functionality. * Implement member listing in `StudentGroupingDetailScreen` and populate it via `StudentGroupingDetailViewModel`. * Fix route parameter naming from `guid` to `classUid` in `StudentGroupingEdit` and related logic. * Remove unused database schema file. --- .../view/clazz/detail/ClazzDetailScreen.kt | 72 +- .../detail/StudentGroupingDetailScreen.kt | 58 +- .../edit/StudentGroupingEditScreen.kt | 1 - .../13.json | 2499 ----------------- .../db/school/xapi/XapiActorDataSourceDb.kt | 37 + .../db/school/xapi/daos/XapiActorDao.kt | 10 +- .../school/xapi/XapiActorDataSource.kt | 4 + .../respect/shared/navigation/AppRoutes.kt | 5 +- .../clazz/detail/ClazzDetailViewModel.kt | 101 +- .../detail/StudentGroupingDetailViewModel.kt | 59 +- .../edit/StudentGroupingEditViewModel.kt | 6 +- 11 files changed, 272 insertions(+), 2580 deletions(-) delete mode 100644 respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 5ab2c7fbf..02989e226 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Groups import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material3.ListItem import androidx.compose.material3.Text @@ -76,6 +77,7 @@ fun ClazzDetailScreen( onTogglePendingSection = viewModel::onTogglePendingSection, onToggleTeachersSection = viewModel::onToggleTeachersSection, onToggleStudentsSection = viewModel::onToggleStudentsSection, + onToggleStudentGroupingSection = viewModel::onToggleStudentGroupingSection, onClickRemovePersonFromClass = viewModel::onClickRemovePersonFromClass, onClickManageEnrollments = viewModel::onClickManageEnrollments, onClickPerson = viewModel::onClickPerson, @@ -94,6 +96,7 @@ fun ClazzDetailScreen( onTogglePendingSection: () -> Unit, onToggleTeachersSection: () -> Unit, onToggleStudentsSection: () -> Unit, + onToggleStudentGroupingSection: () -> Unit, onClickRemovePersonFromClass: (Person, EnrollmentRoleEnum) -> Unit, onClickManageEnrollments: (Person, EnrollmentRoleEnum) -> Unit, onClickPerson: (Person) -> Unit, @@ -362,8 +365,7 @@ fun ClazzDetailScreen( item("student_grouping_header") { ListItem( - modifier = Modifier - .clickable {}, + modifier = Modifier.clickable { onToggleStudentGroupingSection() }, headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), @@ -376,39 +378,63 @@ fun ClazzDetailScreen( trailingContent = { Icon( imageVector = Icons.Outlined.KeyboardArrowDown, - contentDescription = if (uiState.showStudentGrouping) { + contentDescription = if (uiState.isStudentGroupingExpanded) { stringResource(Res.string.collapse_students) } else { stringResource(Res.string.expand_students) }, modifier = Modifier.size(24.dp) .rotate( - if (uiState.showStudentGrouping) 0f else -90f + if (uiState.isStudentGroupingExpanded) 0f else -90f ) ) } ) } - item("student_grouping") { - ListItem( - modifier = Modifier.padding(bottom = 40.dp).clickable { - onClickCreateGroup() - }, - leadingContent = { - Icon( - modifier = Modifier.size(40.dp).padding(8.dp), - imageVector = Icons.Filled.Add, - contentDescription = stringResource(resource = Res.string.create_group) - ) - }, - headlineContent = { - Text( - text = - stringResource(resource = Res.string.create_group) - ) - } - ) + if (uiState.isStudentGroupingExpanded) { + item("student_grouping") { + ListItem( + modifier = Modifier.clickable { + onClickCreateGroup() + }, + leadingContent = { + Icon( + modifier = Modifier.size(40.dp).padding(8.dp), + imageVector = Icons.Filled.Add, + contentDescription = stringResource(resource = Res.string.create_group) + ) + }, + headlineContent = { + Text( + text = + stringResource(resource = Res.string.create_group) + ) + } + ) + } + + items(uiState.groups.size) { index -> + val groupData = uiState.groups[index] + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { }, + leadingContent = { + Icon( + modifier = Modifier.size(40.dp).padding(8.dp), + imageVector = Icons.Filled.Groups, + contentDescription = "" + ) + }, + headlineContent = { + Text(text = groupData.groupName) + }, + supportingContent = { + Text(text = "${groupData.memberCount} ${stringResource(Res.string.students)}") + } + ) + } } } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 73a63041c..9d53bb809 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -1,6 +1,9 @@ package world.respect.app.view.studentgrouping.detail import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Groups import androidx.compose.material3.Icon @@ -9,6 +12,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.material3.Text +import world.respect.app.components.RespectPersonAvatar import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailUiState import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel @@ -19,25 +24,50 @@ fun StudentGroupingDetailScreen( ) { val uiState by viewModel.uiState.collectAsState() StudentGroupingDetailScreen( - uiState = uiState + uiState = uiState, + onClickBack = viewModel::onClickBack ) } @Composable fun StudentGroupingDetailScreen( - uiState: StudentGroupingDetailUiState + uiState: StudentGroupingDetailUiState, + onClickBack: () -> Unit = {} ) { - ListItem( - modifier = Modifier.clickable {}, - leadingContent = { - Icon(Icons.Default.Groups, contentDescription = "") - }, - headlineContent = { - //description of group - }, - supportingContent = { - //number of students in the group + LazyColumn( + modifier = Modifier + .fillMaxSize() + ) { + item { + + ListItem( + modifier = Modifier.clickable {}, + leadingContent = { + Icon(Icons.Default.Groups, contentDescription = "") + }, + headlineContent = { + Text( + text = "${uiState.groupMembers.size} Students", + ) + }, + supportingContent = { + //number of students in the group + } + ) + } - ) -} + items(uiState.groupMembers.size) { index -> + ListItem( + modifier = Modifier.fillMaxWidth(), + leadingContent = { + RespectPersonAvatar(name = uiState.groupMembers[index]) + }, + headlineContent = { + Text(text = uiState.groupMembers[index]) + }, + ) + } + + } +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 2a8840b2c..dd69aed66 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -24,7 +24,6 @@ import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.description import world.respect.shared.generated.resources.name import world.respect.shared.generated.resources.required import world.respect.shared.util.ext.asUiText diff --git a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json b/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json deleted file mode 100644 index 588e5bb93..000000000 --- a/respect-datalayer-db/schemas/world.respect.datalayer.db.RespectSchoolDatabase/13.json +++ /dev/null @@ -1,2499 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 13, - "identityHash": "54c7aa0baa9afa1102cc81beb51e1978", - "entities": [ - { - "tableName": "SchoolAppEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`saUid` TEXT NOT NULL, `saUidNum` INTEGER NOT NULL, `saManifestUrl` TEXT NOT NULL, `saStatus` INTEGER NOT NULL, `saLastModified` INTEGER NOT NULL, `saStored` INTEGER NOT NULL, PRIMARY KEY(`saUidNum`))", - "fields": [ - { - "fieldPath": "saUid", - "columnName": "saUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "saUidNum", - "columnName": "saUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saManifestUrl", - "columnName": "saManifestUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "saStatus", - "columnName": "saStatus", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saLastModified", - "columnName": "saLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saStored", - "columnName": "saStored", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "saUidNum" - ] - } - }, - { - "tableName": "PersonEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pGuid` TEXT NOT NULL, `pGuidHash` INTEGER NOT NULL, `pActive` INTEGER NOT NULL, `pStatus` INTEGER NOT NULL, `pLastModified` INTEGER NOT NULL, `pStored` INTEGER NOT NULL, `pMetadata` TEXT, `pUsername` TEXT, `pGivenName` TEXT NOT NULL, `pFamilyName` TEXT NOT NULL, `pMiddleName` TEXT, `pGender` INTEGER NOT NULL, `pDateOfBirth` INTEGER, `pEmail` TEXT, `pPhoneNumber` TEXT, PRIMARY KEY(`pGuidHash`))", - "fields": [ - { - "fieldPath": "pGuid", - "columnName": "pGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pGuidHash", - "columnName": "pGuidHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pActive", - "columnName": "pActive", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pStatus", - "columnName": "pStatus", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pLastModified", - "columnName": "pLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pStored", - "columnName": "pStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pMetadata", - "columnName": "pMetadata", - "affinity": "TEXT" - }, - { - "fieldPath": "pUsername", - "columnName": "pUsername", - "affinity": "TEXT" - }, - { - "fieldPath": "pGivenName", - "columnName": "pGivenName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pFamilyName", - "columnName": "pFamilyName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pMiddleName", - "columnName": "pMiddleName", - "affinity": "TEXT" - }, - { - "fieldPath": "pGender", - "columnName": "pGender", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pDateOfBirth", - "columnName": "pDateOfBirth", - "affinity": "INTEGER" - }, - { - "fieldPath": "pEmail", - "columnName": "pEmail", - "affinity": "TEXT" - }, - { - "fieldPath": "pPhoneNumber", - "columnName": "pPhoneNumber", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "pGuidHash" - ] - } - }, - { - "tableName": "PersonRoleEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prPersonGuidHash` INTEGER NOT NULL, `prIsPrimaryRole` INTEGER NOT NULL, `prRoleEnum` INTEGER NOT NULL, `prBeginDate` INTEGER, `prEndDate` INTEGER)", - "fields": [ - { - "fieldPath": "prUid", - "columnName": "prUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "prPersonGuidHash", - "columnName": "prPersonGuidHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "prIsPrimaryRole", - "columnName": "prIsPrimaryRole", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "prRoleEnum", - "columnName": "prRoleEnum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "prBeginDate", - "columnName": "prBeginDate", - "affinity": "INTEGER" - }, - { - "fieldPath": "prEndDate", - "columnName": "prEndDate", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "prUid" - ] - } - }, - { - "tableName": "PersonRelatedPersonEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`prpUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `prpPersonUidNum` INTEGER NOT NULL, `prpOtherPersonUid` TEXT NOT NULL, `prpOtherPersonUidNum` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "prpUid", - "columnName": "prpUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "prpPersonUidNum", - "columnName": "prpPersonUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "prpOtherPersonUid", - "columnName": "prpOtherPersonUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "prpOtherPersonUidNum", - "columnName": "prpOtherPersonUidNum", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "prpUid" - ] - } - }, - { - "tableName": "PersonPasswordEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppwGuidNum` INTEGER NOT NULL, `ppwGuid` TEXT NOT NULL, `authAlgorithm` TEXT NOT NULL, `authEncoded` TEXT NOT NULL, `authSalt` TEXT NOT NULL, `authIterations` INTEGER NOT NULL, `authKeyLen` INTEGER NOT NULL, `ppwLastModified` INTEGER NOT NULL, `ppwStored` INTEGER NOT NULL, PRIMARY KEY(`ppwGuidNum`))", - "fields": [ - { - "fieldPath": "ppwGuidNum", - "columnName": "ppwGuidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppwGuid", - "columnName": "ppwGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authAlgorithm", - "columnName": "authAlgorithm", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authEncoded", - "columnName": "authEncoded", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authSalt", - "columnName": "authSalt", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authIterations", - "columnName": "authIterations", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "authKeyLen", - "columnName": "authKeyLen", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppwLastModified", - "columnName": "ppwLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppwStored", - "columnName": "ppwStored", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "ppwGuidNum" - ] - } - }, - { - "tableName": "PersonPasskeyEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ppPersonUidNum` INTEGER NOT NULL, `ppCredentialId` TEXT NOT NULL, `ppLastModified` INTEGER NOT NULL, `ppStored` INTEGER NOT NULL, `ppAttestationObj` TEXT, `ppClientDataJson` TEXT, `ppOriginString` TEXT, `ppChallengeString` TEXT, `ppPublicKey` TEXT, `isRevoked` INTEGER NOT NULL, `ppDeviceName` TEXT NOT NULL DEFAULT '', `ppTimeCreated` INTEGER NOT NULL DEFAULT 0, `ppAaguid` TEXT NOT NULL DEFAULT '', `ppProviderName` TEXT NOT NULL DEFAULT '', `ppIconLight` TEXT NOT NULL DEFAULT '', `ppIconDark` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`ppPersonUidNum`, `ppCredentialId`))", - "fields": [ - { - "fieldPath": "ppPersonUidNum", - "columnName": "ppPersonUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppCredentialId", - "columnName": "ppCredentialId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ppLastModified", - "columnName": "ppLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppStored", - "columnName": "ppStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppAttestationObj", - "columnName": "ppAttestationObj", - "affinity": "TEXT" - }, - { - "fieldPath": "ppClientDataJson", - "columnName": "ppClientDataJson", - "affinity": "TEXT" - }, - { - "fieldPath": "ppOriginString", - "columnName": "ppOriginString", - "affinity": "TEXT" - }, - { - "fieldPath": "ppChallengeString", - "columnName": "ppChallengeString", - "affinity": "TEXT" - }, - { - "fieldPath": "ppPublicKey", - "columnName": "ppPublicKey", - "affinity": "TEXT" - }, - { - "fieldPath": "isRevoked", - "columnName": "isRevoked", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ppDeviceName", - "columnName": "ppDeviceName", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "ppTimeCreated", - "columnName": "ppTimeCreated", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "ppAaguid", - "columnName": "ppAaguid", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "ppProviderName", - "columnName": "ppProviderName", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "ppIconLight", - "columnName": "ppIconLight", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "ppIconDark", - "columnName": "ppIconDark", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "ppPersonUidNum", - "ppCredentialId" - ] - } - }, - { - "tableName": "AuthTokenEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`atUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `atPGuidHash` INTEGER NOT NULL, `atPGuid` TEXT NOT NULL, `atCode` TEXT, `atToken` TEXT NOT NULL, `atTimeCreated` INTEGER NOT NULL, `atTtl` INTEGER NOT NULL, `atPlatform` TEXT, `atAndroidSdkInt` INTEGER, `atVersion` TEXT, `atManufacturer` TEXT, `atModel` TEXT, `atRam` INTEGER)", - "fields": [ - { - "fieldPath": "atUid", - "columnName": "atUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "atPGuidHash", - "columnName": "atPGuidHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "atPGuid", - "columnName": "atPGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "atCode", - "columnName": "atCode", - "affinity": "TEXT" - }, - { - "fieldPath": "atToken", - "columnName": "atToken", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "atTimeCreated", - "columnName": "atTimeCreated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "atTtl", - "columnName": "atTtl", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "atPlatform", - "columnName": "atPlatform", - "affinity": "TEXT" - }, - { - "fieldPath": "atAndroidSdkInt", - "columnName": "atAndroidSdkInt", - "affinity": "INTEGER" - }, - { - "fieldPath": "atVersion", - "columnName": "atVersion", - "affinity": "TEXT" - }, - { - "fieldPath": "atManufacturer", - "columnName": "atManufacturer", - "affinity": "TEXT" - }, - { - "fieldPath": "atModel", - "columnName": "atModel", - "affinity": "TEXT" - }, - { - "fieldPath": "atRam", - "columnName": "atRam", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "atUid" - ] - } - }, - { - "tableName": "ReportEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rGuid` TEXT NOT NULL, `rOwnerGuid` TEXT NOT NULL, `rTitle` TEXT NOT NULL, `rOptions` TEXT NOT NULL, `rIsTemplate` INTEGER NOT NULL, `rActive` INTEGER NOT NULL, `rLastModified` INTEGER NOT NULL, `rStored` INTEGER NOT NULL, PRIMARY KEY(`rGuid`))", - "fields": [ - { - "fieldPath": "rGuid", - "columnName": "rGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rOwnerGuid", - "columnName": "rOwnerGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rTitle", - "columnName": "rTitle", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rOptions", - "columnName": "rOptions", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rIsTemplate", - "columnName": "rIsTemplate", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rActive", - "columnName": "rActive", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rLastModified", - "columnName": "rLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rStored", - "columnName": "rStored", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "rGuid" - ] - } - }, - { - "tableName": "IndicatorEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`indicatorId` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `type` TEXT NOT NULL, `sql` TEXT NOT NULL, PRIMARY KEY(`indicatorId`))", - "fields": [ - { - "fieldPath": "indicatorId", - "columnName": "indicatorId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sql", - "columnName": "sql", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "indicatorId" - ] - } - }, - { - "tableName": "ClassEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cGuid` TEXT NOT NULL, `cGuidHash` INTEGER NOT NULL, `cTitle` TEXT NOT NULL, `cStatus` INTEGER NOT NULL, `cDescription` TEXT, `cLastModified` INTEGER NOT NULL, `cStored` INTEGER NOT NULL, `cTeacherInviteGuid` TEXT, `cStudentInviteGuid` TEXT, PRIMARY KEY(`cGuidHash`))", - "fields": [ - { - "fieldPath": "cGuid", - "columnName": "cGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cGuidHash", - "columnName": "cGuidHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "cTitle", - "columnName": "cTitle", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cStatus", - "columnName": "cStatus", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "cDescription", - "columnName": "cDescription", - "affinity": "TEXT" - }, - { - "fieldPath": "cLastModified", - "columnName": "cLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "cStored", - "columnName": "cStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "cTeacherInviteGuid", - "columnName": "cTeacherInviteGuid", - "affinity": "TEXT" - }, - { - "fieldPath": "cStudentInviteGuid", - "columnName": "cStudentInviteGuid", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "cGuidHash" - ] - } - }, - { - "tableName": "ClassPermissionEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cpeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cpeClassUidNum` INTEGER NOT NULL, `cpeToEnrollmentRole` INTEGER, `cpePermissions` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "cpeId", - "columnName": "cpeId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "cpeClassUidNum", - "columnName": "cpeClassUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "cpeToEnrollmentRole", - "columnName": "cpeToEnrollmentRole", - "affinity": "INTEGER" - }, - { - "fieldPath": "cpePermissions", - "columnName": "cpePermissions", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "cpeId" - ] - } - }, - { - "tableName": "EnrollmentEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eUid` TEXT NOT NULL, `eUidNum` INTEGER NOT NULL, `eStatus` INTEGER NOT NULL, `eLastModified` INTEGER NOT NULL, `eStored` INTEGER NOT NULL, `eMetadata` TEXT, `eClassUid` TEXT NOT NULL, `eClassUidNum` INTEGER NOT NULL, `ePersonUid` TEXT NOT NULL, `ePersonUidNum` INTEGER NOT NULL, `eRole` INTEGER NOT NULL, `eBeginDate` INTEGER, `eEndDate` INTEGER, `eRemovedAt` INTEGER, `eInviteCode` TEXT, `eApprovedByPersonUidNum` INTEGER NOT NULL, `eApprovedByPersonUid` TEXT, PRIMARY KEY(`eUidNum`))", - "fields": [ - { - "fieldPath": "eUid", - "columnName": "eUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "eUidNum", - "columnName": "eUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eStatus", - "columnName": "eStatus", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eLastModified", - "columnName": "eLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eStored", - "columnName": "eStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eMetadata", - "columnName": "eMetadata", - "affinity": "TEXT" - }, - { - "fieldPath": "eClassUid", - "columnName": "eClassUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "eClassUidNum", - "columnName": "eClassUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ePersonUid", - "columnName": "ePersonUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ePersonUidNum", - "columnName": "ePersonUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eRole", - "columnName": "eRole", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eBeginDate", - "columnName": "eBeginDate", - "affinity": "INTEGER" - }, - { - "fieldPath": "eEndDate", - "columnName": "eEndDate", - "affinity": "INTEGER" - }, - { - "fieldPath": "eRemovedAt", - "columnName": "eRemovedAt", - "affinity": "INTEGER" - }, - { - "fieldPath": "eInviteCode", - "columnName": "eInviteCode", - "affinity": "TEXT" - }, - { - "fieldPath": "eApprovedByPersonUidNum", - "columnName": "eApprovedByPersonUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "eApprovedByPersonUid", - "columnName": "eApprovedByPersonUid", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "eUidNum" - ] - } - }, - { - "tableName": "AssignmentEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aeUid` TEXT NOT NULL, `aeUidNum` INTEGER NOT NULL, `aeTitle` TEXT NOT NULL, `aeDescription` TEXT NOT NULL, `aeClassUid` TEXT NOT NULL, `aeClassUidNum` INTEGER NOT NULL, `aeDeadline` INTEGER, `aeLastModified` INTEGER NOT NULL, `aeStored` INTEGER NOT NULL, PRIMARY KEY(`aeUidNum`))", - "fields": [ - { - "fieldPath": "aeUid", - "columnName": "aeUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "aeUidNum", - "columnName": "aeUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aeTitle", - "columnName": "aeTitle", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "aeDescription", - "columnName": "aeDescription", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "aeClassUid", - "columnName": "aeClassUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "aeClassUidNum", - "columnName": "aeClassUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aeDeadline", - "columnName": "aeDeadline", - "affinity": "INTEGER" - }, - { - "fieldPath": "aeLastModified", - "columnName": "aeLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aeStored", - "columnName": "aeStored", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "aeUidNum" - ] - } - }, - { - "tableName": "AssignmentLearningResourceRefEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alrrAeUidNum` INTEGER NOT NULL, `alrrLearningUnitManifestUrlHash` INTEGER NOT NULL, `alrrLearningUnitManifestUrl` TEXT NOT NULL, `alrrAppManifestUrl` TEXT NOT NULL, PRIMARY KEY(`alrrAeUidNum`, `alrrLearningUnitManifestUrlHash`))", - "fields": [ - { - "fieldPath": "alrrAeUidNum", - "columnName": "alrrAeUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "alrrLearningUnitManifestUrlHash", - "columnName": "alrrLearningUnitManifestUrlHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "alrrLearningUnitManifestUrl", - "columnName": "alrrLearningUnitManifestUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "alrrAppManifestUrl", - "columnName": "alrrAppManifestUrl", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "alrrAeUidNum", - "alrrLearningUnitManifestUrlHash" - ] - } - }, - { - "tableName": "WriteQueueItemEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`wqiQueueItemId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `wqiModel` INTEGER NOT NULL, `wqiUid` TEXT NOT NULL, `wqiTimeQueued` INTEGER NOT NULL, `wqiAttemptCount` INTEGER NOT NULL, `wqiTimeWritten` INTEGER NOT NULL, `wqiAccountGuid` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "wqiQueueItemId", - "columnName": "wqiQueueItemId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "wqiModel", - "columnName": "wqiModel", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "wqiUid", - "columnName": "wqiUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wqiTimeQueued", - "columnName": "wqiTimeQueued", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "wqiAttemptCount", - "columnName": "wqiAttemptCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "wqiTimeWritten", - "columnName": "wqiTimeWritten", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "wqiAccountGuid", - "columnName": "wqiAccountGuid", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "wqiQueueItemId" - ] - } - }, - { - "tableName": "SchoolPermissionGrantEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spgUid` TEXT NOT NULL, `spgUidNum` INTEGER NOT NULL, `spgStatusEnum` INTEGER NOT NULL, `spgToRole` INTEGER NOT NULL, `spgPermissions` INTEGER NOT NULL, `spgLastModified` INTEGER NOT NULL, `spgStored` INTEGER NOT NULL, PRIMARY KEY(`spgUidNum`))", - "fields": [ - { - "fieldPath": "spgUid", - "columnName": "spgUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "spgUidNum", - "columnName": "spgUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spgStatusEnum", - "columnName": "spgStatusEnum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spgToRole", - "columnName": "spgToRole", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spgPermissions", - "columnName": "spgPermissions", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spgLastModified", - "columnName": "spgLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spgStored", - "columnName": "spgStored", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "spgUidNum" - ] - } - }, - { - "tableName": "PullSyncStatusEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pssAccountPersonUid` TEXT NOT NULL, `pssAccountPersonUidNum` INTEGER NOT NULL, `pssTableId` INTEGER NOT NULL, `pssLastConsistentThrough` INTEGER NOT NULL, `pssPermissionsLastModified` INTEGER NOT NULL, PRIMARY KEY(`pssAccountPersonUid`, `pssTableId`))", - "fields": [ - { - "fieldPath": "pssAccountPersonUid", - "columnName": "pssAccountPersonUid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pssAccountPersonUidNum", - "columnName": "pssAccountPersonUidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pssTableId", - "columnName": "pssTableId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pssLastConsistentThrough", - "columnName": "pssLastConsistentThrough", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pssPermissionsLastModified", - "columnName": "pssPermissionsLastModified", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "pssAccountPersonUid", - "pssTableId" - ] - } - }, - { - "tableName": "PersonQrBadgeEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pqrGuidNum` INTEGER NOT NULL, `pqrGuid` TEXT NOT NULL, `pqrLastModified` INTEGER NOT NULL, `pqrStored` INTEGER NOT NULL, `pqrQrCodeUrl` TEXT, `pqrStatus` INTEGER NOT NULL, PRIMARY KEY(`pqrGuidNum`))", - "fields": [ - { - "fieldPath": "pqrGuidNum", - "columnName": "pqrGuidNum", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pqrGuid", - "columnName": "pqrGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "pqrLastModified", - "columnName": "pqrLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pqrStored", - "columnName": "pqrStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "pqrQrCodeUrl", - "columnName": "pqrQrCodeUrl", - "affinity": "TEXT" - }, - { - "fieldPath": "pqrStatus", - "columnName": "pqrStatus", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "pqrGuidNum" - ] - }, - "indices": [ - { - "name": "index_PersonQrBadgeEntity_pqrQrCodeUrl", - "unique": false, - "columnNames": [ - "pqrQrCodeUrl" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_PersonQrBadgeEntity_pqrQrCodeUrl` ON `${TABLE_NAME}` (`pqrQrCodeUrl`)" - } - ] - }, - { - "tableName": "InviteEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`iGuid` TEXT NOT NULL, `iGuidHash` INTEGER NOT NULL, `iCode` TEXT NOT NULL, `iApprovalRequiredAfter` INTEGER NOT NULL, `iLastModified` INTEGER NOT NULL, `iStored` INTEGER NOT NULL, `iStatus` INTEGER NOT NULL, `iNewUserRole` INTEGER, `iNewUserFirstInvite` INTEGER NOT NULL, `iForFamilyOfGuid` TEXT, `iForFamilyOfGuidHash` INTEGER, `iForClassGuid` TEXT, `iForClassName` TEXT, `iInviteMode` INTEGER, `iSchoolName` TEXT, `iForClassGuidHash` INTEGER, `iForClassRole` INTEGER, PRIMARY KEY(`iGuidHash`))", - "fields": [ - { - "fieldPath": "iGuid", - "columnName": "iGuid", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "iGuidHash", - "columnName": "iGuidHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "iCode", - "columnName": "iCode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "iApprovalRequiredAfter", - "columnName": "iApprovalRequiredAfter", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "iLastModified", - "columnName": "iLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "iStored", - "columnName": "iStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "iStatus", - "columnName": "iStatus", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "iNewUserRole", - "columnName": "iNewUserRole", - "affinity": "INTEGER" - }, - { - "fieldPath": "iNewUserFirstInvite", - "columnName": "iNewUserFirstInvite", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "iForFamilyOfGuid", - "columnName": "iForFamilyOfGuid", - "affinity": "TEXT" - }, - { - "fieldPath": "iForFamilyOfGuidHash", - "columnName": "iForFamilyOfGuidHash", - "affinity": "INTEGER" - }, - { - "fieldPath": "iForClassGuid", - "columnName": "iForClassGuid", - "affinity": "TEXT" - }, - { - "fieldPath": "iForClassName", - "columnName": "iForClassName", - "affinity": "TEXT" - }, - { - "fieldPath": "iInviteMode", - "columnName": "iInviteMode", - "affinity": "INTEGER" - }, - { - "fieldPath": "iSchoolName", - "columnName": "iSchoolName", - "affinity": "TEXT" - }, - { - "fieldPath": "iForClassGuidHash", - "columnName": "iForClassGuidHash", - "affinity": "INTEGER" - }, - { - "fieldPath": "iForClassRole", - "columnName": "iForClassRole", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "iGuidHash" - ] - } - }, - { - "tableName": "LangMapEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`lmeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `lmeTopParentType` INTEGER NOT NULL, `lmeTopParentUid1` INTEGER NOT NULL, `lmeTopParentUid2` INTEGER NOT NULL, `lmePropType` INTEGER NOT NULL, `lmePropFk` INTEGER NOT NULL, `lmeLang` TEXT NOT NULL, `lmeRegion` TEXT, `lmeValue` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "lmeId", - "columnName": "lmeId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lmeTopParentType", - "columnName": "lmeTopParentType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lmeTopParentUid1", - "columnName": "lmeTopParentUid1", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lmeTopParentUid2", - "columnName": "lmeTopParentUid2", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lmePropType", - "columnName": "lmePropType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lmePropFk", - "columnName": "lmePropFk", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lmeLang", - "columnName": "lmeLang", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lmeRegion", - "columnName": "lmeRegion", - "affinity": "TEXT" - }, - { - "fieldPath": "lmeValue", - "columnName": "lmeValue", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "lmeId" - ] - }, - "indices": [ - { - "name": "index_LangMapEntity_lmeTopParentType_lmeTopParentUid1_lmeTopParentUid2_lmePropType", - "unique": false, - "columnNames": [ - "lmeTopParentType", - "lmeTopParentUid1", - "lmeTopParentUid2", - "lmePropType" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LangMapEntity_lmeTopParentType_lmeTopParentUid1_lmeTopParentUid2_lmePropType` ON `${TABLE_NAME}` (`lmeTopParentType`, `lmeTopParentUid1`, `lmeTopParentUid2`, `lmePropType`)" - } - ] - }, - { - "tableName": "ReadiumLinkEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rleId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rleOpdsParentType` INTEGER NOT NULL, `rleOpdsParentUid` INTEGER NOT NULL, `rlePropType` TEXT NOT NULL, `rlePropFk` INTEGER NOT NULL, `rleIndex` INTEGER NOT NULL, `rleHref` TEXT NOT NULL, `rleRel` TEXT, `rleType` TEXT, `rleTitle` TEXT, `rleTemplated` INTEGER, `rleProperties` TEXT, `rleHeight` INTEGER, `rleWidth` INTEGER, `rleSize` INTEGER, `rleBitrate` REAL, `rleDuration` REAL, `rleLanguage` TEXT)", - "fields": [ - { - "fieldPath": "rleId", - "columnName": "rleId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rleOpdsParentType", - "columnName": "rleOpdsParentType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rleOpdsParentUid", - "columnName": "rleOpdsParentUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rlePropType", - "columnName": "rlePropType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rlePropFk", - "columnName": "rlePropFk", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rleIndex", - "columnName": "rleIndex", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rleHref", - "columnName": "rleHref", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rleRel", - "columnName": "rleRel", - "affinity": "TEXT" - }, - { - "fieldPath": "rleType", - "columnName": "rleType", - "affinity": "TEXT" - }, - { - "fieldPath": "rleTitle", - "columnName": "rleTitle", - "affinity": "TEXT" - }, - { - "fieldPath": "rleTemplated", - "columnName": "rleTemplated", - "affinity": "INTEGER" - }, - { - "fieldPath": "rleProperties", - "columnName": "rleProperties", - "affinity": "TEXT" - }, - { - "fieldPath": "rleHeight", - "columnName": "rleHeight", - "affinity": "INTEGER" - }, - { - "fieldPath": "rleWidth", - "columnName": "rleWidth", - "affinity": "INTEGER" - }, - { - "fieldPath": "rleSize", - "columnName": "rleSize", - "affinity": "INTEGER" - }, - { - "fieldPath": "rleBitrate", - "columnName": "rleBitrate", - "affinity": "REAL" - }, - { - "fieldPath": "rleDuration", - "columnName": "rleDuration", - "affinity": "REAL" - }, - { - "fieldPath": "rleLanguage", - "columnName": "rleLanguage", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "rleId" - ] - } - }, - { - "tableName": "OpdsPublicationEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`opeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `opeOfeUid` INTEGER NOT NULL, `opeOgeUid` INTEGER NOT NULL, `opeIndex` INTEGER NOT NULL, `opeUrl` TEXT, `opeUrlHash` INTEGER NOT NULL, `opeLastModified` INTEGER NOT NULL, `opeEtag` TEXT, `opeMdIdentifier` TEXT, `opeMdLanguage` TEXT, `opeMdType` TEXT, `opeMdDescription` TEXT, `opeMdNumberOfPages` INTEGER, `opeMdDuration` REAL)", - "fields": [ - { - "fieldPath": "opeUid", - "columnName": "opeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "opeOfeUid", - "columnName": "opeOfeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "opeOgeUid", - "columnName": "opeOgeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "opeIndex", - "columnName": "opeIndex", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "opeUrl", - "columnName": "opeUrl", - "affinity": "TEXT" - }, - { - "fieldPath": "opeUrlHash", - "columnName": "opeUrlHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "opeLastModified", - "columnName": "opeLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "opeEtag", - "columnName": "opeEtag", - "affinity": "TEXT" - }, - { - "fieldPath": "opeMdIdentifier", - "columnName": "opeMdIdentifier", - "affinity": "TEXT" - }, - { - "fieldPath": "opeMdLanguage", - "columnName": "opeMdLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "opeMdType", - "columnName": "opeMdType", - "affinity": "TEXT" - }, - { - "fieldPath": "opeMdDescription", - "columnName": "opeMdDescription", - "affinity": "TEXT" - }, - { - "fieldPath": "opeMdNumberOfPages", - "columnName": "opeMdNumberOfPages", - "affinity": "INTEGER" - }, - { - "fieldPath": "opeMdDuration", - "columnName": "opeMdDuration", - "affinity": "REAL" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "opeUid" - ] - } - }, - { - "tableName": "ReadiumSubjectEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rseUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rseStringValue` TEXT, `rseTopParentType` INTEGER NOT NULL, `rseTopParentUid` INTEGER NOT NULL, `rseSubjectSortAs` TEXT, `rseSubjectCode` TEXT, `rseSubjectScheme` TEXT, `rseIndex` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "rseUid", - "columnName": "rseUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rseStringValue", - "columnName": "rseStringValue", - "affinity": "TEXT" - }, - { - "fieldPath": "rseTopParentType", - "columnName": "rseTopParentType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rseTopParentUid", - "columnName": "rseTopParentUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "rseSubjectSortAs", - "columnName": "rseSubjectSortAs", - "affinity": "TEXT" - }, - { - "fieldPath": "rseSubjectCode", - "columnName": "rseSubjectCode", - "affinity": "TEXT" - }, - { - "fieldPath": "rseSubjectScheme", - "columnName": "rseSubjectScheme", - "affinity": "TEXT" - }, - { - "fieldPath": "rseIndex", - "columnName": "rseIndex", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "rseUid" - ] - } - }, - { - "tableName": "OpdsFacetEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofaeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ofaeOfeUid` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "ofaeUid", - "columnName": "ofaeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofaeOfeUid", - "columnName": "ofaeOfeUid", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "ofaeUid" - ] - } - }, - { - "tableName": "OpdsGroupEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ogeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ogeOfeUid` INTEGER NOT NULL, `ogeIndex` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "ogeUid", - "columnName": "ogeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ogeOfeUid", - "columnName": "ogeOfeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ogeIndex", - "columnName": "ogeIndex", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "ogeUid" - ] - } - }, - { - "tableName": "OpdsFeedEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofeUid` INTEGER NOT NULL, `ofeUrl` TEXT NOT NULL, `ofeUrlHash` INTEGER NOT NULL, `ofeLastModified` INTEGER NOT NULL, `ofeLastModifiedHeader` INTEGER NOT NULL, `ofeEtag` TEXT, `ofeStored` INTEGER NOT NULL, PRIMARY KEY(`ofeUid`))", - "fields": [ - { - "fieldPath": "ofeUid", - "columnName": "ofeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofeUrl", - "columnName": "ofeUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ofeUrlHash", - "columnName": "ofeUrlHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofeLastModified", - "columnName": "ofeLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofeLastModifiedHeader", - "columnName": "ofeLastModifiedHeader", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofeEtag", - "columnName": "ofeEtag", - "affinity": "TEXT" - }, - { - "fieldPath": "ofeStored", - "columnName": "ofeStored", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "ofeUid" - ] - } - }, - { - "tableName": "OpdsFeedMetadataEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ofmeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ofmeOfeUid` INTEGER NOT NULL, `ofmePropType` INTEGER NOT NULL, `ofmePropFk` INTEGER NOT NULL, `ofmeIdentifier` TEXT, `ofmeType` TEXT, `ofmeTitle` TEXT NOT NULL, `ofmeSubtitle` TEXT, `ofmeModified` INTEGER, `ofmeDescription` TEXT, `ofmeItemsPerPage` INTEGER, `ofmeCurrentPage` INTEGER, `ofmeNumberOfItems` INTEGER)", - "fields": [ - { - "fieldPath": "ofmeUid", - "columnName": "ofmeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofmeOfeUid", - "columnName": "ofmeOfeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofmePropType", - "columnName": "ofmePropType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofmePropFk", - "columnName": "ofmePropFk", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "ofmeIdentifier", - "columnName": "ofmeIdentifier", - "affinity": "TEXT" - }, - { - "fieldPath": "ofmeType", - "columnName": "ofmeType", - "affinity": "TEXT" - }, - { - "fieldPath": "ofmeTitle", - "columnName": "ofmeTitle", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ofmeSubtitle", - "columnName": "ofmeSubtitle", - "affinity": "TEXT" - }, - { - "fieldPath": "ofmeModified", - "columnName": "ofmeModified", - "affinity": "INTEGER" - }, - { - "fieldPath": "ofmeDescription", - "columnName": "ofmeDescription", - "affinity": "TEXT" - }, - { - "fieldPath": "ofmeItemsPerPage", - "columnName": "ofmeItemsPerPage", - "affinity": "INTEGER" - }, - { - "fieldPath": "ofmeCurrentPage", - "columnName": "ofmeCurrentPage", - "affinity": "INTEGER" - }, - { - "fieldPath": "ofmeNumberOfItems", - "columnName": "ofmeNumberOfItems", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "ofmeUid" - ] - } - }, - { - "tableName": "XapiActivityEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actUid` INTEGER NOT NULL, `actIdIri` TEXT NOT NULL, `actType` TEXT, `actMoreInfo` TEXT, `actInteractionType` INTEGER, `actCorrectResponsePatterns` TEXT, `actNonSignificantLastModified` INTEGER NOT NULL, `actSignificantLastModified` INTEGER NOT NULL, `actStored` INTEGER NOT NULL, `actFlags` INTEGER NOT NULL, `actObjectTypeSet` INTEGER NOT NULL, PRIMARY KEY(`actUid`))", - "fields": [ - { - "fieldPath": "actUid", - "columnName": "actUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actIdIri", - "columnName": "actIdIri", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "actType", - "columnName": "actType", - "affinity": "TEXT" - }, - { - "fieldPath": "actMoreInfo", - "columnName": "actMoreInfo", - "affinity": "TEXT" - }, - { - "fieldPath": "actInteractionType", - "columnName": "actInteractionType", - "affinity": "INTEGER" - }, - { - "fieldPath": "actCorrectResponsePatterns", - "columnName": "actCorrectResponsePatterns", - "affinity": "TEXT" - }, - { - "fieldPath": "actNonSignificantLastModified", - "columnName": "actNonSignificantLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actSignificantLastModified", - "columnName": "actSignificantLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actStored", - "columnName": "actStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actFlags", - "columnName": "actFlags", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actObjectTypeSet", - "columnName": "actObjectTypeSet", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "actUid" - ] - } - }, - { - "tableName": "XapiActivityExtensionEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aeeActivityUid` INTEGER NOT NULL, `aeeKeyHash` INTEGER NOT NULL, `aeeKey` TEXT NOT NULL, `aeeJson` TEXT NOT NULL, `aeeLastMod` INTEGER NOT NULL, `aeeIsDeleted` INTEGER NOT NULL, PRIMARY KEY(`aeeActivityUid`, `aeeKeyHash`))", - "fields": [ - { - "fieldPath": "aeeActivityUid", - "columnName": "aeeActivityUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aeeKeyHash", - "columnName": "aeeKeyHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aeeKey", - "columnName": "aeeKey", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "aeeJson", - "columnName": "aeeJson", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "aeeLastMod", - "columnName": "aeeLastMod", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aeeIsDeleted", - "columnName": "aeeIsDeleted", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "aeeActivityUid", - "aeeKeyHash" - ] - } - }, - { - "tableName": "XapiActivityInteractionEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aieUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `aieActivityUid` INTEGER NOT NULL, `aieProp` INTEGER NOT NULL, `aieId` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "aieUid", - "columnName": "aieUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aieActivityUid", - "columnName": "aieActivityUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aieProp", - "columnName": "aieProp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "aieId", - "columnName": "aieId", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "aieUid" - ] - }, - "indices": [ - { - "name": "index_XapiActivityInteractionEntity_aieActivityUid", - "unique": false, - "columnNames": [ - "aieActivityUid" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_XapiActivityInteractionEntity_aieActivityUid` ON `${TABLE_NAME}` (`aieActivityUid`)" - } - ] - }, - { - "tableName": "XapiActivityLangMapEntry", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`almeUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `almeActivityUid` INTEGER NOT NULL, `almeProperty` INTEGER NOT NULL, `almeInteractionId` TEXT, `almeLangCode` TEXT NOT NULL, `almeValue` TEXT NOT NULL, `almeLastModified` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "almeUid", - "columnName": "almeUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "almeActivityUid", - "columnName": "almeActivityUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "almeProperty", - "columnName": "almeProperty", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "almeInteractionId", - "columnName": "almeInteractionId", - "affinity": "TEXT" - }, - { - "fieldPath": "almeLangCode", - "columnName": "almeLangCode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "almeValue", - "columnName": "almeValue", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "almeLastModified", - "columnName": "almeLastModified", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "almeUid" - ] - }, - "indices": [ - { - "name": "index_XapiActivityLangMapEntry_almeActivityUid_almeProperty_almeInteractionId_almeLangCode", - "unique": true, - "columnNames": [ - "almeActivityUid", - "almeProperty", - "almeInteractionId", - "almeLangCode" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_XapiActivityLangMapEntry_almeActivityUid_almeProperty_almeInteractionId_almeLangCode` ON `${TABLE_NAME}` (`almeActivityUid`, `almeProperty`, `almeInteractionId`, `almeLangCode`)" - } - ] - }, - { - "tableName": "XapiActorEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actorUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `actorPersonUid` INTEGER NOT NULL, `actorName` TEXT, `actorMbox` TEXT, `actorMbox_sha1sum` TEXT, `actorOpenid` TEXT, `actorAccountName` TEXT, `actorAccountHomePage` TEXT, `actorStored` INTEGER NOT NULL, `actorLastModified` INTEGER NOT NULL, `actorGroupMembersLastUpdated` INTEGER NOT NULL, `actorObjectType` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "actorUid", - "columnName": "actorUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actorPersonUid", - "columnName": "actorPersonUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actorName", - "columnName": "actorName", - "affinity": "TEXT" - }, - { - "fieldPath": "actorMbox", - "columnName": "actorMbox", - "affinity": "TEXT" - }, - { - "fieldPath": "actorMbox_sha1sum", - "columnName": "actorMbox_sha1sum", - "affinity": "TEXT" - }, - { - "fieldPath": "actorOpenid", - "columnName": "actorOpenid", - "affinity": "TEXT" - }, - { - "fieldPath": "actorAccountName", - "columnName": "actorAccountName", - "affinity": "TEXT" - }, - { - "fieldPath": "actorAccountHomePage", - "columnName": "actorAccountHomePage", - "affinity": "TEXT" - }, - { - "fieldPath": "actorStored", - "columnName": "actorStored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actorLastModified", - "columnName": "actorLastModified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actorGroupMembersLastUpdated", - "columnName": "actorGroupMembersLastUpdated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "actorObjectType", - "columnName": "actorObjectType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "actorUid" - ] - }, - "indices": [ - { - "name": "idx_actorentity_uid_personuid", - "unique": false, - "columnNames": [ - "actorPersonUid" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `idx_actorentity_uid_personuid` ON `${TABLE_NAME}` (`actorPersonUid`)" - }, - { - "name": "idx_actorentity_actorobjecttype", - "unique": false, - "columnNames": [ - "actorObjectType" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `idx_actorentity_actorobjecttype` ON `${TABLE_NAME}` (`actorObjectType`)" - } - ] - }, - { - "tableName": "XapiGroupMemberActorJoin", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gmajGroupActorUid` INTEGER NOT NULL, `gmajMemberActorUid` INTEGER NOT NULL, PRIMARY KEY(`gmajGroupActorUid`, `gmajMemberActorUid`))", - "fields": [ - { - "fieldPath": "gmajGroupActorUid", - "columnName": "gmajGroupActorUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "gmajMemberActorUid", - "columnName": "gmajMemberActorUid", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "gmajGroupActorUid", - "gmajMemberActorUid" - ] - }, - "indices": [ - { - "name": "idx_groupmemberactorjoin_gmajgroupactoruid", - "unique": false, - "columnNames": [ - "gmajGroupActorUid" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `idx_groupmemberactorjoin_gmajgroupactoruid` ON `${TABLE_NAME}` (`gmajGroupActorUid`)" - }, - { - "name": "idx_groupmemberactorjoin_gmajmemberactoruid", - "unique": false, - "columnNames": [ - "gmajMemberActorUid" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `idx_groupmemberactorjoin_gmajmemberactoruid` ON `${TABLE_NAME}` (`gmajMemberActorUid`)" - } - ] - }, - { - "tableName": "XapiStatementContextActivityJoin", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scajUid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `scajFromStatementIdHi` INTEGER NOT NULL, `scajFromStatementIdLo` INTEGER NOT NULL, `scajContextType` INTEGER NOT NULL, `scajToActivityUid` INTEGER NOT NULL, `scajToActivityId` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "scajUid", - "columnName": "scajUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "scajFromStatementIdHi", - "columnName": "scajFromStatementIdHi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "scajFromStatementIdLo", - "columnName": "scajFromStatementIdLo", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "scajContextType", - "columnName": "scajContextType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "scajToActivityUid", - "columnName": "scajToActivityUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "scajToActivityId", - "columnName": "scajToActivityId", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "scajUid" - ] - } - }, - { - "tableName": "XapiStatementEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`statementIdHi` INTEGER NOT NULL, `statementIdLo` INTEGER NOT NULL, `statementActorPersonUid` INTEGER NOT NULL, `statementVerbUid` INTEGER NOT NULL, `statementVerbId` TEXT NOT NULL, `statementObjectType` INTEGER NOT NULL, `statementObjectActivityId` TEXT, `statementObjectUid1` INTEGER NOT NULL, `statementObjectUid2` INTEGER NOT NULL, `statementActorUid` INTEGER NOT NULL, `authorityActorUid` INTEGER NOT NULL, `resultCompletion` INTEGER, `resultSuccess` INTEGER, `resultScoreScaled` REAL, `resultScoreRaw` REAL, `resultScoreMin` REAL, `resultScoreMax` REAL, `resultDuration` INTEGER, `resultResponse` TEXT, `resultExtensions` TEXT, `timestamp` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `contextRegistrationHi` INTEGER NOT NULL, `contextRegistrationLo` INTEGER NOT NULL, `contextRegistrationHash` INTEGER NOT NULL, `contextLanguage` TEXT, `contextPlatform` TEXT, `contextRevision` TEXT, `contextStatementRefIdHi` INTEGER NOT NULL, `contextStatementRefIdLo` INTEGER NOT NULL, `contextInstructorActorUid` INTEGER NOT NULL, `contextTeamActorUid` INTEGER NOT NULL, `extensionProgress` INTEGER, `completionOrProgress` INTEGER NOT NULL, `statementContentEntryUid` INTEGER NOT NULL, `isSubStatement` INTEGER NOT NULL, `statementVersion` TEXT, PRIMARY KEY(`statementIdHi`, `statementIdLo`))", - "fields": [ - { - "fieldPath": "statementIdHi", - "columnName": "statementIdHi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementIdLo", - "columnName": "statementIdLo", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementActorPersonUid", - "columnName": "statementActorPersonUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementVerbUid", - "columnName": "statementVerbUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementVerbId", - "columnName": "statementVerbId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "statementObjectType", - "columnName": "statementObjectType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementObjectActivityId", - "columnName": "statementObjectActivityId", - "affinity": "TEXT" - }, - { - "fieldPath": "statementObjectUid1", - "columnName": "statementObjectUid1", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementObjectUid2", - "columnName": "statementObjectUid2", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementActorUid", - "columnName": "statementActorUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "authorityActorUid", - "columnName": "authorityActorUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "resultCompletion", - "columnName": "resultCompletion", - "affinity": "INTEGER" - }, - { - "fieldPath": "resultSuccess", - "columnName": "resultSuccess", - "affinity": "INTEGER" - }, - { - "fieldPath": "resultScoreScaled", - "columnName": "resultScoreScaled", - "affinity": "REAL" - }, - { - "fieldPath": "resultScoreRaw", - "columnName": "resultScoreRaw", - "affinity": "REAL" - }, - { - "fieldPath": "resultScoreMin", - "columnName": "resultScoreMin", - "affinity": "REAL" - }, - { - "fieldPath": "resultScoreMax", - "columnName": "resultScoreMax", - "affinity": "REAL" - }, - { - "fieldPath": "resultDuration", - "columnName": "resultDuration", - "affinity": "INTEGER" - }, - { - "fieldPath": "resultResponse", - "columnName": "resultResponse", - "affinity": "TEXT" - }, - { - "fieldPath": "resultExtensions", - "columnName": "resultExtensions", - "affinity": "TEXT" - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "stored", - "columnName": "stored", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextRegistrationHi", - "columnName": "contextRegistrationHi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextRegistrationLo", - "columnName": "contextRegistrationLo", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextRegistrationHash", - "columnName": "contextRegistrationHash", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextLanguage", - "columnName": "contextLanguage", - "affinity": "TEXT" - }, - { - "fieldPath": "contextPlatform", - "columnName": "contextPlatform", - "affinity": "TEXT" - }, - { - "fieldPath": "contextRevision", - "columnName": "contextRevision", - "affinity": "TEXT" - }, - { - "fieldPath": "contextStatementRefIdHi", - "columnName": "contextStatementRefIdHi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextStatementRefIdLo", - "columnName": "contextStatementRefIdLo", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextInstructorActorUid", - "columnName": "contextInstructorActorUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contextTeamActorUid", - "columnName": "contextTeamActorUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "extensionProgress", - "columnName": "extensionProgress", - "affinity": "INTEGER" - }, - { - "fieldPath": "completionOrProgress", - "columnName": "completionOrProgress", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementContentEntryUid", - "columnName": "statementContentEntryUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isSubStatement", - "columnName": "isSubStatement", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statementVersion", - "columnName": "statementVersion", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "statementIdHi", - "statementIdLo" - ] - }, - "indices": [ - { - "name": "idx_stmt_actor_person", - "unique": false, - "columnNames": [ - "statementActorPersonUid" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `idx_stmt_actor_person` ON `${TABLE_NAME}` (`statementActorPersonUid`)" - } - ] - }, - { - "tableName": "XapiStatementEntityJson", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stmtJsonIdHi` INTEGER NOT NULL, `stmtJsonIdLo` INTEGER NOT NULL, `fullStatement` TEXT, PRIMARY KEY(`stmtJsonIdHi`, `stmtJsonIdLo`))", - "fields": [ - { - "fieldPath": "stmtJsonIdHi", - "columnName": "stmtJsonIdHi", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "stmtJsonIdLo", - "columnName": "stmtJsonIdLo", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "fullStatement", - "columnName": "fullStatement", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "stmtJsonIdHi", - "stmtJsonIdLo" - ] - } - }, - { - "tableName": "XapiVerbEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`verbUid` INTEGER NOT NULL, `verbUrlId` TEXT NOT NULL, `verbStatus` INTEGER NOT NULL, `verbLct` INTEGER NOT NULL, PRIMARY KEY(`verbUid`))", - "fields": [ - { - "fieldPath": "verbUid", - "columnName": "verbUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "verbUrlId", - "columnName": "verbUrlId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "verbStatus", - "columnName": "verbStatus", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "verbLct", - "columnName": "verbLct", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "verbUid" - ] - } - }, - { - "tableName": "XapiVerbLangMapEntry", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`vlmeVerbUid` INTEGER NOT NULL, `vlmeLangCode` TEXT NOT NULL, `vlmeEntryString` TEXT NOT NULL, PRIMARY KEY(`vlmeVerbUid`, `vlmeLangCode`))", - "fields": [ - { - "fieldPath": "vlmeVerbUid", - "columnName": "vlmeVerbUid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "vlmeLangCode", - "columnName": "vlmeLangCode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "vlmeEntryString", - "columnName": "vlmeEntryString", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "vlmeVerbUid", - "vlmeLangCode" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '54c7aa0baa9afa1102cc81beb51e1978')" - ] - } -} \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt index 4ac9ac83c..8e311deb7 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt @@ -4,7 +4,9 @@ import world.respect.datalayer.AuthenticatedUserPrincipalId import world.respect.datalayer.DataLoadParams import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase +import world.respect.datalayer.db.school.xapi.adapters.ActorEntities import world.respect.datalayer.db.school.xapi.adapters.toEntities +import world.respect.datalayer.db.school.xapi.adapters.toModel import world.respect.datalayer.ext.EPOCH import world.respect.datalayer.school.xapi.XapiActorDataSourceLocal import world.respect.datalayer.school.xapi.model.XapiActor @@ -77,4 +79,39 @@ class XapiActorDataSourceDb( ) { TODO("Not yet implemented") } + + override suspend fun getGroupDetail(groupId: String): XapiGroup? { + // Find the group actor by its accountName (groupId) + val groupActorEntity = schoolDb.getActorDao().findGroupByAccountNameAsync(groupId) + ?: return null + + // Find all group member joins for this group + val groupMemberJoins = schoolDb.getGroupMemberActorJoinDao() + .findByGroupActorUidList(listOf(groupActorEntity.actorUid)) + + // Find all member actor entities + val memberActorUids = groupMemberJoins.map { it.gmajMemberActorUid } + val memberActorEntities = if (memberActorUids.isNotEmpty()) { + schoolDb.getActorDao().findByUidList(memberActorUids) + } else { + emptyList() + } + + // Convert to model using adapter + val actorEntities = ActorEntities( + actor = groupActorEntity, + groupMemberAgents = memberActorEntities, + groupMemberJoins = groupMemberJoins + ) + + return actorEntities.toModel() as? XapiGroup + } + + override suspend fun getGroupsByIds(groupIds: List): List { + if (groupIds.isEmpty()) return emptyList() + + return groupIds.mapNotNull { groupId -> + getGroupDetail(groupId) + } + } } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt index 49fbe6ea3..a861decaf 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt @@ -80,5 +80,13 @@ interface XapiActorDao { uids: List ): List - + @Query( + """ + SELECT XapiActorEntity.* + FROM XapiActorEntity + WHERE XapiActorEntity.actorAccountName = :accountName + LIMIT 1 + """ + ) + suspend fun findGroupByAccountNameAsync(accountName: String): XapiActorEntity? } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt index 9ba149801..09f7a5a86 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt @@ -2,6 +2,7 @@ package world.respect.datalayer.school.xapi import world.respect.datalayer.DataLoadParams import world.respect.datalayer.school.xapi.model.XapiActor +import world.respect.datalayer.school.xapi.model.XapiGroup interface XapiActorDataSource { @@ -10,4 +11,7 @@ interface XapiActorDataSource { dataLoadParams: DataLoadParams = DataLoadParams(), ) + suspend fun getGroupDetail(groupId: String): XapiGroup? + + suspend fun getGroupsByIds(groupIds: List): List } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 232224166..c79819556 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -221,12 +221,13 @@ data class ClazzEdit( @Serializable data class StudentGroupingEdit( - val guid: String + val classUid: String ) : RespectAppRoute @Serializable data class StudentGroupingDetail( - val groupId: String + val groupId: String, + val classId: String ): RespectAppRoute @Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 6d31669f3..0c818a05c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -88,6 +88,14 @@ data class ClazzDetailUiState( val showAddTeacher: Boolean = false, val showStudentGrouping: Boolean = false, val isStudentGroupingExpanded: Boolean = true, + val groupIds: List = emptyList(), + val groups: List = emptyList(), +) + +data class GroupDisplayData( + val groupId: String, + val groupName: String, + val memberCount: Int, ) class ClazzDetailViewModel( @@ -176,7 +184,26 @@ class ClazzDetailViewModel( _appUiState.update { it.copy(title = clazz.dataOrNull()?.title?.asUiText()) } - _uiState.update { it.copy(clazz = clazz) } + + // Extract group IDs from metadata + val clazzData = clazz.dataOrNull() + val groupIds = if (clazzData != null) { + extractGroupIdsFromMetadata(clazzData) + } else { + emptyList() + } + + _uiState.update { + it.copy( + clazz = clazz, + groupIds = groupIds + ) + } + + // Load group details for each group ID + if (groupIds.isNotEmpty()) { + loadGroupDetails(groupIds) + } } } @@ -298,6 +325,10 @@ class ClazzDetailViewModel( _uiState.update { it.copy(isStudentsExpanded = !it.isStudentsExpanded) } } + fun onToggleStudentGroupingSection() { + _uiState.update { it.copy(isStudentGroupingExpanded = !it.isStudentGroupingExpanded) } + } + fun onClickEdit() { _navCommandFlow.tryEmit( NavCommand.Navigate(ClazzEdit(route.guid)) @@ -353,35 +384,13 @@ class ClazzDetailViewModel( ) } fun onClickCreateGroup() { - viewModelScope.launch { - try { - val students = schoolDataSource.personDataSource.list( - loadParams = DataLoadParams(), - params = PersonDataSource.GetListParams( - filterByClazzUid = route.guid, - filterByEnrolmentRole = EnrollmentRoleEnum.STUDENT, - inClassOnDay = localDateInCurrentTimeZone(), - ) - ).dataOrNull() - - if (students.isNullOrEmpty()) { - snackBarDispatcher.showSnackBar( - Snack(Res.string.add_student_to_create_group.asUiText()) - )} else { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - StudentGroupingEdit(guid = route.guid) - ) - ) - } - } catch (e: Throwable) { - Napier.e("onClickCreateGroup ERROR", throwable = e) - snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) - } - } + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingEdit(classUid = route.guid) + ) + ) } - fun onClickManageEnrollments(person: Person, role: EnrollmentRoleEnum) { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -394,6 +403,42 @@ class ClazzDetailViewModel( ) } + private fun extractGroupIdsFromMetadata(clazz: Clazz): List { + return try { + val metadata = clazz.metadata ?: return emptyList() + val groupIdsJsonArray = metadata["groupIds"] as? kotlinx.serialization.json.JsonArray + groupIdsJsonArray?.map { it.toString().trim('"') } ?: emptyList() + } catch (e: Throwable) { + Napier.w("Failed to extract group IDs from metadata", throwable = e) + emptyList() + } + } + + private fun loadGroupDetails(groupIds: List) { + viewModelScope.launch { + try { + val groups = schoolDataSource.xapiActorDataSource.getGroupsByIds(groupIds) + val groupDisplayDataList = groups.map { group -> + val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + Napier.i("Loaded Group: ${group.name}") + Napier.i("Group Members: ${memberNames.joinToString(", ")}") + + GroupDisplayData( + groupId = group.account?.name ?: "", + groupName = group.name ?: "", + memberCount = memberNames.size + ) + } + + _uiState.update { prev -> + prev.copy(groups = groupDisplayDataList) + } + } catch (e: Throwable) { + Napier.e("loadGroupDetails ERROR", throwable = e) + } + } + } + companion object { const val ALL = "All" diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index c63b99161..01bc16fcf 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -12,19 +12,20 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.ext.dataOrNull import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group +import world.respect.shared.navigation.ClazzDetail +import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.StudentGroupingDetail -import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import kotlin.getValue data class StudentGroupingDetailUiState( - val str: String = "" + val groupName: String = "", + val groupMembers: List = emptyList() ) class StudentGroupingDetailViewModel( @@ -43,13 +44,53 @@ class StudentGroupingDetailViewModel( private val route: StudentGroupingDetail = savedStateHandle.toRoute() init { - _appUiState.update { - it.copy( - title = Res.string.create_group.asUiText(), - userAccountIconVisible = false, - hideBottomNavigation = true, - ) + loadGroupDetail() + } + + private fun loadGroupDetail() { + viewModelScope.launch { + try { + val group = schoolDataSource.xapiActorDataSource.getGroupDetail(route.groupId) + + if (group != null) { + // Extract member names from the group + val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + + // Print group details and members + Napier.i("Group Name: ${group.name}") + Napier.i("Number of Members: ${memberNames.size}") + memberNames.forEachIndexed { index, memberName -> + Napier.i("Member ${index + 1}: $memberName") + } + + // Update UI state with group information + _uiState.update { prev -> + prev.copy( + groupName = group.name ?: "", + groupMembers = memberNames + ) + } + } + + _appUiState.update { + it.copy( + title = group?.name?.asUiText() ?: Res.string.create_group.asUiText(), + userAccountIconVisible = false, + hideBottomNavigation = true, + ) + } + } catch (e: Throwable) { + Napier.e("loadGroupDetail ERROR", throwable = e) + } } } + + fun onClickBack() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + ClazzDetail(route.classId), popUpTo = route, popUpToInclusive = true + ) + ) + } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 2dd976b5b..395ff322b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -88,7 +88,7 @@ class StudentGroupingEditViewModel( schoolDataSource.personDataSource.listAsPagingSource( loadParams = DataLoadParams(), params = PersonDataSource.GetListParams( - filterByClazzUid = route.guid, + filterByClazzUid = route.classUid, filterByEnrolmentRole = role, inClassOnDay = localDateInCurrentTimeZone(), ) @@ -126,7 +126,7 @@ class StudentGroupingEditViewModel( } viewModelScope.launch { - schoolDataSource.classDataSource.findByGuidAsFlow(route.guid).collect { clazz -> + schoolDataSource.classDataSource.findByGuidAsFlow(route.classUid).collect { clazz -> _uiState.update { it.copy(clazz = clazz) } } } @@ -224,7 +224,7 @@ class StudentGroupingEditViewModel( schoolDataSource.classDataSource.store(listOf(updatedClazz)) - _navCommandFlow.tryEmit(NavCommand.Navigate(StudentGroupingDetail(groupId))) + _navCommandFlow.tryEmit(NavCommand.Navigate(StudentGroupingDetail(groupId = groupId, classId = route.classUid))) } catch (e: Throwable) { Napier.e("onClickSave ERROR", throwable = e) } From e9e6de857dab8c0f31093601178212f2e46fc47b Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 28 Apr 2026 11:20:23 +0530 Subject: [PATCH 018/130] Implement student grouping features in class and group detail screens. * Add `getGroupDetail` and `getGroupsByIds` to `XapiActorDataSource` and its database implementation. * Update `ClazzDetailViewModel` to extract and load group details from class metadata. * Enhance `ClazzDetailScreen` to display a list of student groups with toggle functionality. * Implement member listing in `StudentGroupingDetailScreen` and populate it via `StudentGroupingDetailViewModel`. * Fix route parameter naming from `guid` to `classUid` in `StudentGroupingEdit` and related logic. * Remove unused database schema file. --- .../004_001_add_groups_to_a_class_test.yaml | 3 - .../view/clazz/detail/ClazzDetailScreen.kt | 10 +- .../detail/StudentGroupingDetailScreen.kt | 37 +++++-- .../edit/StudentGroupingEditScreen.kt | 2 +- .../db/school/xapi/XapiActorDataSourceDb.kt | 6 ++ .../school/xapi/XapiActorDataSource.kt | 5 + .../datalayer/school/xapi/model/XapiGroup.kt | 6 ++ .../composeResources/values/strings.xml | 1 + .../respect/shared/navigation/AppRoutes.kt | 3 +- .../clazz/detail/ClazzDetailViewModel.kt | 45 +++++--- .../detail/StudentGroupingDetailViewModel.kt | 55 ++++++---- .../edit/StudentGroupingEditViewModel.kt | 102 ++++++++++++------ 12 files changed, 191 insertions(+), 84 deletions(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index b751e4f6e..50d1f6519 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -59,15 +59,12 @@ onFlowComplete: text: "Create group" - tapOn: "Name*" - inputText: "Test Group" -- tapOn: "Description" -- inputText: "This test group functionality" - tapOn: "StudentA USER" - tapOn: "StudentC USER" - tapOn: "Save" - assertVisible: id: "app_title" text: "Test Group" -- assertVisible: "This test group functionality" - assertVisible: "StudentA USER" - assertVisible: "StudentC USER" - assertVisible: diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 02989e226..e1bfc74a7 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -81,7 +81,8 @@ fun ClazzDetailScreen( onClickRemovePersonFromClass = viewModel::onClickRemovePersonFromClass, onClickManageEnrollments = viewModel::onClickManageEnrollments, onClickPerson = viewModel::onClickPerson, - onClickCreateGroup=viewModel::onClickCreateGroup + onClickCreateGroup=viewModel::onClickCreateGroup, + onClickGroup = viewModel::onClickGroup ) } @@ -100,7 +101,8 @@ fun ClazzDetailScreen( onClickRemovePersonFromClass: (Person, EnrollmentRoleEnum) -> Unit, onClickManageEnrollments: (Person, EnrollmentRoleEnum) -> Unit, onClickPerson: (Person) -> Unit, - onClickCreateGroup:()-> Unit + onClickCreateGroup:()-> Unit, + onClickGroup: (String) -> Unit, ) { val teacherPager = respectRememberPager(uiState.teachers) val studentPager = respectRememberPager(uiState.students) @@ -419,7 +421,9 @@ fun ClazzDetailScreen( ListItem( modifier = Modifier .fillMaxWidth() - .clickable { }, + .clickable { + onClickGroup(groupData.groupId) + }, leadingContent = { Icon( modifier = Modifier.size(40.dp).padding(8.dp), diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 9d53bb809..e0a670e8f 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -1,11 +1,14 @@ package world.respect.app.view.studentgrouping.detail import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.runtime.Composable @@ -13,7 +16,13 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.material3.Text +import androidx.compose.ui.platform.testTag +import org.jetbrains.compose.resources.stringResource import world.respect.app.components.RespectPersonAvatar +import world.respect.app.components.defaultItemPadding +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.delete +import world.respect.shared.generated.resources.student import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailUiState import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel @@ -24,15 +33,13 @@ fun StudentGroupingDetailScreen( ) { val uiState by viewModel.uiState.collectAsState() StudentGroupingDetailScreen( - uiState = uiState, - onClickBack = viewModel::onClickBack + uiState = uiState ) } @Composable fun StudentGroupingDetailScreen( - uiState: StudentGroupingDetailUiState, - onClickBack: () -> Unit = {} + uiState: StudentGroupingDetailUiState ) { LazyColumn( @@ -48,15 +55,28 @@ fun StudentGroupingDetailScreen( }, headlineContent = { Text( - text = "${uiState.groupMembers.size} Students", + text = "${uiState.groupMembers.size} ${stringResource(Res.string.student)}", ) - }, - supportingContent = { - //number of students in the group } ) } + + item("divider1") { + HorizontalDivider() + } + + item { + Column (modifier = Modifier.fillMaxWidth().testTag("delete_group_btn").defaultItemPadding()){ + Icon(Icons.Outlined.Delete, contentDescription = "") + Text(stringResource(Res.string.delete)) + } + } + + item("divider2 ") { + HorizontalDivider() + } + items(uiState.groupMembers.size) { index -> ListItem( modifier = Modifier.fillMaxWidth(), @@ -68,6 +88,5 @@ fun StudentGroupingDetailScreen( }, ) } - } } \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index dd69aed66..51644e90d 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -93,7 +93,7 @@ fun StudentGroupingEditScreen( } ) { student -> - val isSelected = student?.guid in uiState.selectedStudentIds + val isSelected = student?.fullName() in uiState.selectedStudentNames PersonListItemWithMenu( person = student, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt index 8e311deb7..78713c0e7 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt @@ -1,5 +1,7 @@ package world.respect.datalayer.db.school.xapi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import world.respect.datalayer.AuthenticatedUserPrincipalId import world.respect.datalayer.DataLoadParams import world.respect.datalayer.UidNumberMapper @@ -114,4 +116,8 @@ class XapiActorDataSourceDb( getGroupDetail(groupId) } } + + override fun getGroupDetailAsFlow(groupId: String): Flow = flow { + emit(getGroupDetail(groupId)) + } } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt index 09f7a5a86..d2f7b70bd 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt @@ -1,5 +1,6 @@ package world.respect.datalayer.school.xapi +import kotlinx.coroutines.flow.Flow import world.respect.datalayer.DataLoadParams import world.respect.datalayer.school.xapi.model.XapiActor import world.respect.datalayer.school.xapi.model.XapiGroup @@ -14,4 +15,8 @@ interface XapiActorDataSource { suspend fun getGroupDetail(groupId: String): XapiGroup? suspend fun getGroupsByIds(groupIds: List): List + + fun getGroupDetailAsFlow(groupId: String): Flow + + } \ No newline at end of file diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiGroup.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiGroup.kt index 3fc8071f5..bc7b30681 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiGroup.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiGroup.kt @@ -25,7 +25,13 @@ data class XapiGroup( get() = name == null && mbox == null && mbox_sha1sum == null && openid == null && account == null + + companion object { + const val RESULT_KEY_GROUP_UPDATED = "groupUpdated" + } + } val XapiGroup.isAnonymous: Boolean get() = mbox == null && openid == null && account == null + diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 7bb93f6d6..f5bc2e194 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -144,6 +144,7 @@ Add Student"> Add Student to create a Group"> Create Group"> + Edit Group"> Groups"> Accept Invite"> Dismiss invite diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index c79819556..118e4ab62 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -221,7 +221,8 @@ data class ClazzEdit( @Serializable data class StudentGroupingEdit( - val classUid: String + val classUid: String, + val groupId:String? ) : RespectAppRoute @Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 0c818a05c..ec640e6ad 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.JsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope @@ -55,7 +56,7 @@ import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.writequeue.EnqueueRunPullSyncUseCase -import world.respect.shared.generated.resources.add_student_to_create_group +import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState @@ -66,10 +67,10 @@ import kotlin.getValue import kotlin.time.Clock data class ClazzDetailUiState( - val teachers: IPagingSourceFactory = EmptyPagingSourceFactory() , + val teachers: IPagingSourceFactory = EmptyPagingSourceFactory(), val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val pendingTeachers:IPagingSourceFactory = EmptyPagingSourceFactory() , - val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory() , + val pendingTeachers: IPagingSourceFactory = EmptyPagingSourceFactory(), + val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory(), val listOfPending: List = emptyList(), val chipOptions: List = emptyList(), @@ -132,9 +133,9 @@ class ClazzDetailViewModel( } } - private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) + private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) - private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) private val teachersPendingPagingSource = pagingSourceByRole(EnrollmentRoleEnum.PENDING_TEACHER) @@ -254,7 +255,7 @@ class ClazzDetailViewModel( ) ) ) - }catch(e: Throwable) { + } catch (e: Throwable) { e.printStackTrace() } } @@ -284,7 +285,7 @@ class ClazzDetailViewModel( ) ) ) - } + } } fun onSortOrderChanged(sortOption: SortOrderOption) { @@ -304,7 +305,7 @@ class ClazzDetailViewModel( personUid = user.guid, approved = true, ) - }catch(e: Throwable) { + } catch (e: Throwable) { e.printStackTrace() } } @@ -335,6 +336,18 @@ class ClazzDetailViewModel( ) } + fun onClickGroup(groupId: String) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingDetail( + groupId = groupId, + classId = route.guid + ) + ) + ) + } + + fun onClickRemovePersonFromClass(person: Person, role: EnrollmentRoleEnum) { viewModelScope.launch { try { @@ -356,9 +369,9 @@ class ClazzDetailViewModel( }.map { it.copy( lastModified = modTime, - status = if(it.beginDate == today) { + status = if (it.beginDate == today) { StatusEnum.TO_BE_DELETED - }else { + } else { it.status }, endDate = today, @@ -368,7 +381,7 @@ class ClazzDetailViewModel( schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) - }catch(e: Throwable) { + } catch (e: Throwable) { //do something Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) @@ -383,10 +396,13 @@ class ClazzDetailViewModel( ) ) } + fun onClickCreateGroup() { _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingEdit(classUid = route.guid) + StudentGroupingEdit( + classUid = route.guid, groupId = null + ) ) ) } @@ -406,7 +422,7 @@ class ClazzDetailViewModel( private fun extractGroupIdsFromMetadata(clazz: Clazz): List { return try { val metadata = clazz.metadata ?: return emptyList() - val groupIdsJsonArray = metadata["groupIds"] as? kotlinx.serialization.json.JsonArray + val groupIdsJsonArray = metadata["groupIds"] as? JsonArray groupIdsJsonArray?.map { it.toString().trim('"') } ?: emptyList() } catch (e: Throwable) { Napier.w("Failed to extract group IDs from metadata", throwable = e) @@ -439,6 +455,7 @@ class ClazzDetailViewModel( } } + companion object { const val ALL = "All" diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 01bc16fcf..eb4f34702 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -12,14 +12,18 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.school.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group -import world.respect.shared.navigation.ClazzDetail +import world.respect.shared.generated.resources.edit import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.NavResultReturner import world.respect.shared.navigation.StudentGroupingDetail +import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue @@ -44,7 +48,38 @@ class StudentGroupingDetailViewModel( private val route: StudentGroupingDetail = savedStateHandle.toRoute() init { + _appUiState.update { + it.copy( + fabState = FabUiState( + visible = true, + icon = FabUiState.FabIcon.EDIT, + text = Res.string.edit.asUiText(), + onClick = ::onClickEdit + ) + ) + } loadGroupDetail() + + // Listen for result from edit screen and refresh + val navResultReturner: NavResultReturner = getKoin().get() + viewModelScope.launch { + navResultReturner.filteredResultFlowForKey( + RESULT_KEY_GROUP_UPDATED + ).collect { + loadGroupDetail() + } + } + } + + fun onClickEdit() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingEdit( + classUid = route.classId, + groupId = route.groupId + ) + ) + ) } private fun loadGroupDetail() { @@ -53,17 +88,8 @@ class StudentGroupingDetailViewModel( val group = schoolDataSource.xapiActorDataSource.getGroupDetail(route.groupId) if (group != null) { - // Extract member names from the group val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - // Print group details and members - Napier.i("Group Name: ${group.name}") - Napier.i("Number of Members: ${memberNames.size}") - memberNames.forEachIndexed { index, memberName -> - Napier.i("Member ${index + 1}: $memberName") - } - - // Update UI state with group information _uiState.update { prev -> prev.copy( groupName = group.name ?: "", @@ -84,13 +110,4 @@ class StudentGroupingDetailViewModel( } } } - - fun onClickBack() { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - ClazzDetail(route.classId), popUpTo = route, popUpToInclusive = true - ) - ) - } } - diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 395ff322b..0fa5e1cdc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -31,6 +31,7 @@ import world.respect.datalayer.school.xapi.model.VERB_CREATED import world.respect.datalayer.school.xapi.model.XapiAccount import world.respect.datalayer.school.xapi.model.XapiAgent import world.respect.datalayer.school.xapi.model.XapiGroup +import world.respect.datalayer.school.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.datalayer.school.xapi.model.XapiObjectType import world.respect.datalayer.school.xapi.model.XapiStatement import world.respect.datalayer.school.xapi.model.XapiVerb @@ -41,6 +42,7 @@ import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.account.RespectSessionAndPerson import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.edit_group import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.required_field import world.respect.shared.generated.resources.save @@ -68,40 +70,43 @@ data class StudentGroupingEditUiState( val clazz: DataLoadState = DataLoadingState(), ) - class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, var respectAccountManager: RespectAccountManager, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { + + override val scope: Scope = respectAccountManager.requireActiveAccountScope() private val _uiState = MutableStateFlow(StudentGroupingEditUiState()) - val uiState = _uiState.asStateFlow() private val schoolDataSource: SchoolDataSource by inject() - private val route: StudentGroupingEdit = savedStateHandle.toRoute() - private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { + private fun pagingSourceByRole(): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { schoolDataSource.personDataSource.listAsPagingSource( loadParams = DataLoadParams(), params = PersonDataSource.GetListParams( filterByClazzUid = route.classUid, - filterByEnrolmentRole = role, + filterByEnrolmentRole = EnrollmentRoleEnum.STUDENT, inClassOnDay = localDateInCurrentTimeZone(), ) ) } } - private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + private val studentPagingSource = pagingSourceByRole() init { _appUiState.update { it.copy( - title = Res.string.create_group.asUiText(), + title = if (route.groupId == null) + Res.string.create_group.asUiText() + else + Res.string.edit_group.asUiText(), + userAccountIconVisible = false, hideBottomNavigation = true, actionBarButtonState = ActionBarButtonUiState( @@ -111,10 +116,34 @@ class StudentGroupingEditViewModel( ), ) } + _uiState.update { it.copy(students = studentPagingSource) } + route.groupId?.let { groupId -> + viewModelScope.launch { + try { + val group = schoolDataSource.xapiActorDataSource.getGroupDetail(groupId) ?: return@launch + + val memberNames = group.member + ?.mapNotNull { it.name } + ?: emptyList() + + _uiState.update { prev -> + prev.copy( + groupName = group.name ?: "", + selectedStudentNames = memberNames, + ) + } + + } catch (e: Throwable) { + Napier.e("EDIT_GROUP: ERROR loading group", throwable = e) + } + } + } + + // selected user info viewModelScope.launch { respectAccountManager.selectedAccountAndPersonFlow.collect { selectedAccount -> _uiState.update { @@ -125,25 +154,28 @@ class StudentGroupingEditViewModel( } } + // class data viewModelScope.launch { - schoolDataSource.classDataSource.findByGuidAsFlow(route.classUid).collect { clazz -> - _uiState.update { it.copy(clazz = clazz) } - } + schoolDataSource.classDataSource.findByGuidAsFlow(route.classUid) + .collect { clazz -> + _uiState.update { it.copy(clazz = clazz) } + } } } @OptIn(ExperimentalUuidApi::class) fun onClickSave() { + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self val groupName = _uiState.value.groupName if (groupName.isBlank()) { - _uiState.update { prev -> - prev.copy(groupNameError = Res.string.required_field.asUiText()) + _uiState.update { + it.copy(groupNameError = Res.string.required_field.asUiText()) } return } else { - _uiState.update { prev -> prev.copy(groupNameError = null) } + _uiState.update { it.copy(groupNameError = null) } } viewModelScope.launch { @@ -159,7 +191,8 @@ class StudentGroupingEditViewModel( ) } - val groupId = Uuid.random().toString() + val groupId = route.groupId ?: Uuid.random().toString() + val group = XapiGroup( objectType = XapiObjectType.Group, name = groupName, @@ -202,15 +235,13 @@ class StudentGroupingEditViewModel( ?.toMutableList() ?: mutableListOf() - if (!existingGroupIds.contains(groupId)) { + if (route.groupId == null && !existingGroupIds.contains(groupId)) { existingGroupIds.add(groupId) } val updatedMetadata = buildJsonObject { clazz.metadata?.forEach { (key, value) -> - if (key != GROUP_IDS) { - put(key, value) - } + if (key != GROUP_IDS) put(key, value) } putJsonArray(GROUP_IDS) { existingGroupIds.forEach { add(it) } @@ -224,7 +255,18 @@ class StudentGroupingEditViewModel( schoolDataSource.classDataSource.store(listOf(updatedClazz)) - _navCommandFlow.tryEmit(NavCommand.Navigate(StudentGroupingDetail(groupId = groupId, classId = route.classUid))) + if (route.groupId == null) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingDetail(groupId, route.classUid), + popUpTo = route, + popUpToInclusive = true + ) + ) + } else { + sendResultAndPop(RESULT_KEY_GROUP_UPDATED, true) + } + } catch (e: Throwable) { Napier.e("onClickSave ERROR", throwable = e) } @@ -233,33 +275,25 @@ class StudentGroupingEditViewModel( fun onStudentCheckedChange(person: Person, isChecked: Boolean) { _uiState.update { prev -> - - val updatedIds = if (isChecked) { - prev.selectedStudentIds + person.guid - } else { - prev.selectedStudentIds - person.guid - } - + val name = person.fullName() val updatedNames = if (isChecked) { - prev.selectedStudentNames + person.fullName() + prev.selectedStudentNames + name } else { - prev.selectedStudentNames - person.fullName() + prev.selectedStudentNames - name } prev.copy( - selectedStudentIds = updatedIds, selectedStudentNames = updatedNames ) } } fun onGroupNameChanged(name: String) { - _uiState.update { prev -> - prev.copy( + _uiState.update { + it.copy( groupName = name, - groupNameError = if (name.isNotBlank() && prev.groupNameError != null) null else prev.groupNameError + groupNameError = null ) } } -} - +} \ No newline at end of file From e560a97fc48cb075e859a2ce5a7a60dd864db2ad Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 28 Apr 2026 11:46:48 +0530 Subject: [PATCH 019/130] Clean up whitespace .kt --- .../respect/app/components/UstadExposedDropDownMenuField.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt index 1fc933f80..face82962 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt @@ -34,6 +34,7 @@ fun RespectExposedDropDownMenuField( var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( modifier = modifier, expanded = expanded, @@ -74,6 +75,7 @@ fun RespectExposedDropDownMenuField( ) } } + } } From 0b707323022651dd67ca076de4d71db844aae397 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 28 Apr 2026 12:41:00 +0530 Subject: [PATCH 020/130] Update UI accessibility, formatting, and Maestro test flow for student grouping screens. --- .../004_001_add_groups_to_a_class_test.yaml | 3 ++- .../detail/StudentGroupingDetailScreen.kt | 21 +++++++++++++------ .../edit/StudentGroupingEditScreen.kt | 4 +++- .../clazz/detail/ClazzDetailViewModel.kt | 1 - 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index 50d1f6519..ca399638c 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -69,7 +69,8 @@ onFlowComplete: - assertVisible: "StudentC USER" - assertVisible: id: "delete_group_btn" -- tapOn: "floating_action_button" #Edit Button +- tapOn: + id: "floating_action_button" - tapOn: "StudentD USER" - tapOn: "Save" - assertVisible: diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index e0a670e8f..41cf67813 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -51,11 +51,13 @@ fun StudentGroupingDetailScreen( ListItem( modifier = Modifier.clickable {}, leadingContent = { - Icon(Icons.Default.Groups, contentDescription = "") + Icon(Icons.Default.Groups, + contentDescription = null) }, headlineContent = { Text( - text = "${uiState.groupMembers.size} ${stringResource(Res.string.student)}", + text = "${uiState.groupMembers.size} " + + stringResource(Res.string.student), ) } ) @@ -67,10 +69,17 @@ fun StudentGroupingDetailScreen( } item { - Column (modifier = Modifier.fillMaxWidth().testTag("delete_group_btn").defaultItemPadding()){ - Icon(Icons.Outlined.Delete, contentDescription = "") - Text(stringResource(Res.string.delete)) - } + Column( + modifier = Modifier.fillMaxWidth() + .testTag("delete_group_btn") + .defaultItemPadding() + ) { + Icon( + Icons.Outlined.Delete, + contentDescription = stringResource(Res.string.delete) + ) + Text(stringResource(Res.string.delete)) + } } item("divider2 ") { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 51644e90d..c8f7623fc 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -66,7 +66,9 @@ fun StudentGroupingEditScreen( item { OutlinedTextField( - modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag("name"), + modifier = Modifier.fillMaxWidth() + .defaultItemPadding() + .testTag("name"), value = uiState.groupName, onValueChange = { value -> onGroupNameChanged(value) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index ec640e6ad..69d4bbc10 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -201,7 +201,6 @@ class ClazzDetailViewModel( ) } - // Load group details for each group ID if (groupIds.isNotEmpty()) { loadGroupDetails(groupIds) } From af73a5819d2ba65b023bd2a63658b2ec9eedc509 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 28 Apr 2026 12:41:27 +0530 Subject: [PATCH 021/130] Update UI accessibility, formatting, and Maestro test flow for student grouping screens. --- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 0fa5e1cdc..a4a8877ee 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -143,7 +143,6 @@ class StudentGroupingEditViewModel( } } - // selected user info viewModelScope.launch { respectAccountManager.selectedAccountAndPersonFlow.collect { selectedAccount -> _uiState.update { @@ -154,7 +153,6 @@ class StudentGroupingEditViewModel( } } - // class data viewModelScope.launch { schoolDataSource.classDataSource.findByGuidAsFlow(route.classUid) .collect { clazz -> From ec17435e737f2e5ff469c5681861b4f0d645deb9 Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 28 Apr 2026 16:57:12 +0400 Subject: [PATCH 022/130] Update Maestro flows to rename "New Class" to "TestClass" and remove redundant navigation steps in subflows. --- ...01_001_invite_users_using_qr_code_or_link_test.yaml | 10 +++++----- ..._admin_user_assigns_assignment_to_a_class_test.yaml | 4 ++-- .maestro/flows/004_001_add_groups_to_a_class_test.yaml | 8 ++++---- .../flows/subflows/admin_add_class_with_members.yaml | 4 ++-- .../flows/subflows/admin_add_new_student_to_class.yaml | 2 -- .../flows/subflows/admin_add_new_teacher_to_class.yaml | 2 -- .../subflows/create_account_for_class_member.yaml | 5 ----- 7 files changed, 13 insertions(+), 22 deletions(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index 94623f25a..f205000d7 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -105,10 +105,10 @@ onFlowComplete: - runFlow: file: "subflows/admin_add_class.yaml" env: - CLASSNAME: "New Class" + CLASSNAME: "TestClass" - assertVisible: id: "app_title" - text: "New Class" + text: "TestClass" - tapOn: "Add Student" - tapOn: "Invite person" - assertVisible: @@ -140,7 +140,7 @@ onFlowComplete: - assertVisible: "School name" - assertVisible: ${output.SCHOOL_NAME} - assertVisible: "Class name" -- assertVisible: "New Class" +- assertVisible: "TestClass" - assertVisible: "School server URL" - assertVisible: ${output.SCHOOL_URL} - tapOn: "Next" @@ -195,7 +195,7 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Classes" -- tapOn: "New Class" +- tapOn: "TestClass" - assertVisible: "Pending requests.*" - assertVisible: "Student User.*" - tapOn: "Accept Invite" @@ -223,7 +223,7 @@ onFlowComplete: - assertVisible: "School name" - assertVisible: ${output.SCHOOL_NAME} - assertVisible: "Class name" -- assertVisible: "New Class" +- assertVisible: "TestClass" - assertVisible: "School server URL" - assertVisible: ${output.SCHOOL_URL} - tapOn: "Next" diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 163d04183..b6bbe9c06 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -46,7 +46,7 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Classes" -- assertVisible: "New Class" +- assertVisible: "TestClass" - tapOn: "Assignments" - tapOn: id: "floating_action_button" # +Assignment button @@ -58,7 +58,7 @@ onFlowComplete: - tapOn: "Assignment name*" - inputText: "Homework 1" - tapOn: "Class" -- tapOn: "New Class" +- tapOn: "TestClass" - tapOn: "Date" - runScript: file: "scripts/setDate.js" diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index ca399638c..4df7893d1 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -45,11 +45,11 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Classes" -- assertVisible: "New Class" -- tapOn: "New Class" +- assertVisible: "TestClass" +- tapOn: "TestClass" - assertVisible: id: "app_title" - text: "New Class" + text: "TestClass" - scrollUntilVisible: element: "Groups" direction: DOWN @@ -80,6 +80,6 @@ onFlowComplete: - back - assertVisible: id: "app_title" - text: "New Class" + text: "TestClass" - scrollUntilVisible: element: "Test Group" \ No newline at end of file diff --git a/.maestro/flows/subflows/admin_add_class_with_members.yaml b/.maestro/flows/subflows/admin_add_class_with_members.yaml index 18dd30ff9..dc8593345 100644 --- a/.maestro/flows/subflows/admin_add_class_with_members.yaml +++ b/.maestro/flows/subflows/admin_add_class_with_members.yaml @@ -12,7 +12,7 @@ appId: world.respect.app id: "app_title" text: "Add class" - tapOn: "Class name*" -- inputText: "New Class" +- inputText: "TestClass" - tapOn: "Save" - runFlow: @@ -45,6 +45,6 @@ appId: world.respect.app - runFlow: file: "create_account_for_class_member.yaml" env: - CLASSNAME: "New Class" + CLASSNAME: "TestClass" MEMBERNAME: TeacherA User MEMBERUSERNAME: teacherauser diff --git a/.maestro/flows/subflows/admin_add_new_student_to_class.yaml b/.maestro/flows/subflows/admin_add_new_student_to_class.yaml index 06afddd55..c8ba5cfbd 100644 --- a/.maestro/flows/subflows/admin_add_new_student_to_class.yaml +++ b/.maestro/flows/subflows/admin_add_new_student_to_class.yaml @@ -1,8 +1,6 @@ appId: world.respect.app --- -- tapOn: "Classes" -- tapOn: "New Class" - tapOn: "Add Student" - tapOn: "Add person" - assertVisible: diff --git a/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml b/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml index 9c324dcf3..8235c5f83 100644 --- a/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml +++ b/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml @@ -1,8 +1,6 @@ appId: world.respect.app --- -- tapOn: "Classes" -- tapOn: "New Class" - tapOn: "Add Teacher" - tapOn: "Add person" - assertVisible: diff --git a/.maestro/flows/subflows/create_account_for_class_member.yaml b/.maestro/flows/subflows/create_account_for_class_member.yaml index 07f367c0c..7df66b6fb 100644 --- a/.maestro/flows/subflows/create_account_for_class_member.yaml +++ b/.maestro/flows/subflows/create_account_for_class_member.yaml @@ -2,11 +2,6 @@ appId: world.respect.app --- -- tapOn: "Classes" -- assertVisible: - id: "app_title" - text: "Classes" -- tapOn: ${CLASSNAME} - tapOn: ${MEMBERNAME} - tapOn: "Create account" - assertVisible: From eb795da46390b888393523376c140d016f56d680 Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 28 Apr 2026 17:35:45 +0400 Subject: [PATCH 023/130] Update Maestro subflow `admin_add_app_and_teacher.yaml` to change the class name from "New Class" to "TestClass" across all steps and assertions. --- .maestro/flows/subflows/admin_add_app_and_teacher.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.maestro/flows/subflows/admin_add_app_and_teacher.yaml b/.maestro/flows/subflows/admin_add_app_and_teacher.yaml index 0fa24bc63..4f0204858 100644 --- a/.maestro/flows/subflows/admin_add_app_and_teacher.yaml +++ b/.maestro/flows/subflows/admin_add_app_and_teacher.yaml @@ -30,11 +30,11 @@ appId: world.respect.app id: "app_title" text: "Add class" - tapOn: "Class name*" -- inputText: "New Class" +- inputText: "TestClass" - tapOn: "Save" - assertVisible: id: "app_title" - text: "New Class" + text: "TestClass" # Admin copy invite code to the teacher - tapOn: "Add Teacher" @@ -71,7 +71,7 @@ appId: world.respect.app - assertVisible: "School name" - assertVisible: ${output.SCHOOL_NAME} - assertVisible: "Class name" -- assertVisible: "New Class" +- assertVisible: "TestClass" - assertVisible: "School server URL" - assertVisible: ${output.SCHOOL_URL} - tapOn: "Next" @@ -118,7 +118,7 @@ appId: world.respect.app - assertVisible: id: "app_title" text: "Classes" -- tapOn: "New Class" +- tapOn: "TestClass" - assertVisible: "Pending requests.*" - assertVisible: "Teacher User.*" - tapOn: "Accept Invite" From 7e65e026ebcb6c8a380c689b5c3ad33e7cb97aec Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 29 Apr 2026 12:17:01 +0530 Subject: [PATCH 024/130] Implement group deletion in `StudentGroupingDetailScreen`. * Added a delete confirmation dialog to `StudentGroupingDetailScreen` using `RespectBasicAlertDialog`. * Implemented `onClickDeleteGroup`, `onDismissDeleteGroupDialog`, and `onConfirmDeleteGroup` logic in `StudentGroupingDetailViewModel`. * Integrated xAPI statement generation for the "deleted" verb when a group is removed. * Updated `StudentGroupingEditViewModel` to correctly use the "updated" xAPI verb when modifying existing groups. * Added `VERB_DELETED` constant and new string resources for deletion UI. --- .../detail/StudentGroupingDetailScreen.kt | 26 ++++- .../datalayer/school/xapi/model/XapiVerb.kt | 2 + .../composeResources/values/strings.xml | 2 + .../detail/StudentGroupingDetailViewModel.kt | 106 +++++++++++++++++- .../edit/StudentGroupingEditViewModel.kt | 10 +- 5 files changed, 137 insertions(+), 9 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 41cf67813..6be35e82e 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Groups -import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -18,10 +18,14 @@ import androidx.compose.ui.Modifier import androidx.compose.material3.Text import androidx.compose.ui.platform.testTag import org.jetbrains.compose.resources.stringResource +import world.respect.app.components.RespectBasicAlertDialog import world.respect.app.components.RespectPersonAvatar import world.respect.app.components.defaultItemPadding import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.cancel import world.respect.shared.generated.resources.delete +import world.respect.shared.generated.resources.permanently_deleted +import world.respect.shared.generated.resources.permanently_delete_this_group import world.respect.shared.generated.resources.student import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailUiState import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel @@ -33,13 +37,26 @@ fun StudentGroupingDetailScreen( ) { val uiState by viewModel.uiState.collectAsState() StudentGroupingDetailScreen( - uiState = uiState + uiState = uiState, + onClickDelete = viewModel::onClickDeleteGroup, ) + + if (uiState.showDeleteGroupDialog) { + RespectBasicAlertDialog( + headlineText = stringResource(Res.string.permanently_deleted), + bodyText = stringResource(Res.string.permanently_delete_this_group), + onConfirm = viewModel::onConfirmDeleteGroup, + onDismissRequest = viewModel::onDismissDeleteGroupDialog, + confirmText = stringResource(Res.string.delete), + dismissText = stringResource(Res.string.cancel), + ) + } } @Composable fun StudentGroupingDetailScreen( - uiState: StudentGroupingDetailUiState + uiState: StudentGroupingDetailUiState = StudentGroupingDetailUiState(), + onClickDelete: () -> Unit = {}, ) { LazyColumn( @@ -73,9 +90,10 @@ fun StudentGroupingDetailScreen( modifier = Modifier.fillMaxWidth() .testTag("delete_group_btn") .defaultItemPadding() + .clickable { onClickDelete() } ) { Icon( - Icons.Outlined.Delete, + Icons.Default.Delete, contentDescription = stringResource(Res.string.delete) ) Text(stringResource(Res.string.delete)) diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt index e06d10b81..c9f0d27b2 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/model/XapiVerb.kt @@ -11,6 +11,8 @@ const val VERB_CREATED = "http://adlnet.gov/expapi/verbs/create" const val VERB_UPDATED = "http://adlnet.gov/expapi/verbs/update" +const val VERB_DELETED = "http://adlnet.gov/expapi/verbs/deleted" + @Serializable data class XapiVerb( val id: String, diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index f5bc2e194..5cef95ca1 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -461,6 +461,8 @@ Badge assigned Delete this passkey? + Permanently Delete? + Permanently delete this group You could lose access to the account if you dont have another way to sign in. toggle visibility diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index eb4f34702..8d9727a83 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -8,11 +8,25 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.putJsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.ext.dataOrNull +import world.respect.datalayer.school.model.Clazz +import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS +import world.respect.datalayer.school.xapi.model.VERB_DELETED +import world.respect.datalayer.school.xapi.model.XapiAccount +import world.respect.datalayer.school.xapi.model.XapiAgent import world.respect.datalayer.school.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED +import world.respect.datalayer.school.xapi.model.XapiObjectType +import world.respect.datalayer.school.xapi.model.XapiStatement +import world.respect.datalayer.school.xapi.model.XapiVerb import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group @@ -25,16 +39,19 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue +import kotlin.time.Clock +import kotlin.uuid.ExperimentalUuidApi data class StudentGroupingDetailUiState( val groupName: String = "", - val groupMembers: List = emptyList() + val groupMembers: List = emptyList(), + val showDeleteGroupDialog: Boolean = false, ) class StudentGroupingDetailViewModel( savedStateHandle: SavedStateHandle, - respectAccountManager: RespectAccountManager, + var respectAccountManager: RespectAccountManager, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = respectAccountManager.requireActiveAccountScope() @@ -82,6 +99,91 @@ class StudentGroupingDetailViewModel( ) } + fun onClickDeleteGroup() { + _uiState.update { it.copy(showDeleteGroupDialog = true) } + } + + fun onDismissDeleteGroupDialog() { + _uiState.update { it.copy(showDeleteGroupDialog = false) } + } + + @OptIn(ExperimentalUuidApi::class) + fun onConfirmDeleteGroup() { + _uiState.update { it.copy(showDeleteGroupDialog = false) } + + viewModelScope.launch { + try { + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + + val group = schoolDataSource.xapiActorDataSource.getGroupDetail(route.groupId) + ?: return@launch + + val actor = XapiAgent( + name = respectAccountManager.activeAccount?.userGuid ?: "", + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = respectAccountManager.activeAccount?.userGuid ?: "", + homePage = schoolSelfUrl.toString() + ) + ) + + val verb = XapiVerb( + id = VERB_DELETED, + display = mapOf("en" to VERB_DELETED) + ) + + val statement = XapiStatement( + actor = actor, + verb = verb, + `object` = group, + timestamp = Clock.System.now() + ) + + schoolDataSource.xapiStatementDataSource.store(listOf(statement)) + + val clazz = schoolDataSource.classDataSource.findByGuidAsFlow(route.classId) + .let { it -> + var result: Clazz? = null + it.collect { state -> + result = state.dataOrNull() + return@collect + } + result + } + + if (clazz != null) { + val existingGroupIds = clazz.metadata + ?.get(GROUP_IDS) + ?.jsonArray + ?.map { it.jsonPrimitive.content } + ?.filter { it != route.groupId } + ?: emptyList() + + val updatedMetadata = buildJsonObject { + clazz.metadata?.forEach { (key, value) -> + if (key != GROUP_IDS) put(key, value) + } + putJsonArray(GROUP_IDS) { + existingGroupIds.forEach { add(it) } + } + } + + val updatedClazz = clazz.copy( + metadata = updatedMetadata, + lastModified = Clock.System.now() + ) + + schoolDataSource.classDataSource.store(listOf(updatedClazz)) + } + + _navCommandFlow.tryEmit(NavCommand.PopUp()) + + } catch (e: Throwable) { + Napier.e("onConfirmDeleteGroup ERROR", throwable = e) + } + } + } + private fun loadGroupDetail() { viewModelScope.launch { try { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index a4a8877ee..0d9b5ee0e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -28,6 +28,7 @@ import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.xapi.model.VERB_CREATED +import world.respect.datalayer.school.xapi.model.VERB_UPDATED import world.respect.datalayer.school.xapi.model.XapiAccount import world.respect.datalayer.school.xapi.model.XapiAgent import world.respect.datalayer.school.xapi.model.XapiGroup @@ -70,6 +71,7 @@ data class StudentGroupingEditUiState( val clazz: DataLoadState = DataLoadingState(), ) + class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, var respectAccountManager: RespectAccountManager, @@ -123,7 +125,8 @@ class StudentGroupingEditViewModel( route.groupId?.let { groupId -> viewModelScope.launch { try { - val group = schoolDataSource.xapiActorDataSource.getGroupDetail(groupId) ?: return@launch + val group = schoolDataSource.xapiActorDataSource.getGroupDetail(groupId) + ?: return@launch val memberNames = group.member ?.mapNotNull { it.name } @@ -210,9 +213,10 @@ class StudentGroupingEditViewModel( ) ) + val verbId = if (route.groupId != null) VERB_UPDATED else VERB_CREATED val verb = XapiVerb( - id = VERB_CREATED, - display = mapOf("en" to VERB_CREATED) + id = verbId, + display = mapOf("en" to verbId) ) val statement = XapiStatement( From 1e2e689cdb4a71f37aca7ba431a43454ec01b188 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 29 Apr 2026 13:00:03 +0530 Subject: [PATCH 025/130] Update class data loading and group handling in StudentGroupingDetailViewModel and ClazzDetailViewModel. --- .../clazz/detail/ClazzDetailViewModel.kt | 2 + .../detail/StudentGroupingDetailViewModel.kt | 61 ++++++++++--------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 69d4bbc10..621ebea64 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -203,6 +203,8 @@ class ClazzDetailViewModel( if (groupIds.isNotEmpty()) { loadGroupDetails(groupIds) + } else { + _uiState.update { it.copy(groups = emptyList()) } } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 8d9727a83..437fe0f7a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -16,6 +16,8 @@ import kotlinx.serialization.json.putJsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.DataLoadingState import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.model.Clazz @@ -47,6 +49,7 @@ data class StudentGroupingDetailUiState( val groupName: String = "", val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, + val clazz: DataLoadState = DataLoadingState(), ) class StudentGroupingDetailViewModel( @@ -86,6 +89,13 @@ class StudentGroupingDetailViewModel( loadGroupDetail() } } + + viewModelScope.launch { + schoolDataSource.classDataSource.findByGuidAsFlow(route.classId) + .collect { clazz -> + _uiState.update { it.copy(clazz = clazz) } + } + } } fun onClickEdit() { @@ -141,40 +151,31 @@ class StudentGroupingDetailViewModel( schoolDataSource.xapiStatementDataSource.store(listOf(statement)) - val clazz = schoolDataSource.classDataSource.findByGuidAsFlow(route.classId) - .let { it -> - var result: Clazz? = null - it.collect { state -> - result = state.dataOrNull() - return@collect - } - result - } + // Remove group ID from class metadata + val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch + + val existingGroupIds = clazz.metadata + ?.get(GROUP_IDS) + ?.jsonArray + ?.map { it.jsonPrimitive.content } + ?.filter { it != route.groupId } + ?: emptyList() - if (clazz != null) { - val existingGroupIds = clazz.metadata - ?.get(GROUP_IDS) - ?.jsonArray - ?.map { it.jsonPrimitive.content } - ?.filter { it != route.groupId } - ?: emptyList() - - val updatedMetadata = buildJsonObject { - clazz.metadata?.forEach { (key, value) -> - if (key != GROUP_IDS) put(key, value) - } - putJsonArray(GROUP_IDS) { - existingGroupIds.forEach { add(it) } - } + val updatedMetadata = buildJsonObject { + clazz.metadata?.forEach { (key, value) -> + if (key != GROUP_IDS) put(key, value) } + putJsonArray(GROUP_IDS) { + existingGroupIds.forEach { add(it) } + } + } - val updatedClazz = clazz.copy( - metadata = updatedMetadata, - lastModified = Clock.System.now() - ) + val updatedClazz = clazz.copy( + metadata = updatedMetadata, + lastModified = Clock.System.now() + ) - schoolDataSource.classDataSource.store(listOf(updatedClazz)) - } + schoolDataSource.classDataSource.store(listOf(updatedClazz)) _navCommandFlow.tryEmit(NavCommand.PopUp()) From 5f0577133a8ad55e20ad80b3f2c47489ec64594c Mon Sep 17 00:00:00 2001 From: pooja Date: Wed, 29 Apr 2026 11:35:04 +0400 Subject: [PATCH 026/130] Update Maestro flow `004_001_add_groups_to_a_class_test.yaml` to include assertions for student counts and group deletion dialogs, and remove the redundant `admin_add_app` subflow. --- .../flows/004_001_add_groups_to_a_class_test.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index 4df7893d1..d878a930b 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -17,7 +17,6 @@ onFlowComplete: file: "scripts/teardown.js" --- - runFlow: "subflows/school_admin_login_flow.yaml" -- runFlow: "subflows/admin_add_app.yaml" - runFlow: "subflows/admin_add_class_with_members.yaml" - clearState: world.respect.app @@ -67,6 +66,14 @@ onFlowComplete: text: "Test Group" - assertVisible: "StudentA USER" - assertVisible: "StudentC USER" +- assertVisible: "2 Students" +- tapOn: + id: "delete_group_btn" +- assertVisible: "Permanently delete?" +- assertVisible: "Permanently delete this playlist" +- assertVisible: "Cancel" +- assertVisible: "Delete" +- tapOn: "Cancel" - assertVisible: id: "delete_group_btn" - tapOn: @@ -77,9 +84,12 @@ onFlowComplete: id: "app_title" text: "Test Group" - assertVisible: "StudentD USER" +- assertVisible: "3 Students" - back - assertVisible: id: "app_title" text: "TestClass" - scrollUntilVisible: - element: "Test Group" \ No newline at end of file + element: "Test Group (3)" +- assertVisible: "Groups (1)" +- assertVisible: "Test Group (3)" From 30147816f76f1a672932019d08a5ccfacfa567a2 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 29 Apr 2026 13:35:01 +0530 Subject: [PATCH 027/130] Update ClazzDetail and StudentGroupingDetail screens to show member avatars and counts. --- .../view/clazz/detail/ClazzDetailScreen.kt | 47 +++++++++++++------ .../detail/StudentGroupingDetailScreen.kt | 33 +++++++++++-- .../clazz/detail/ClazzDetailViewModel.kt | 4 +- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index e1bfc74a7..adcaadea6 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -1,15 +1,16 @@ package world.respect.app.view.clazz.detail import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Groups import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material3.ListItem import androidx.compose.material3.Text @@ -19,6 +20,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.paging.compose.collectAsLazyPagingItems import org.jetbrains.compose.resources.stringResource import world.respect.app.components.RespectListSortHeader @@ -232,7 +234,10 @@ fun ClazzDetailScreen( headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), - text = stringResource(Res.string.teachers) + text = if (teacherLazyPagingItems.itemCount > 0) + "${stringResource(Res.string.teachers)} (${teacherLazyPagingItems.itemCount})" + else + stringResource(Res.string.teachers) ) }, @@ -299,9 +304,10 @@ fun ClazzDetailScreen( headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), - text = stringResource( - resource = Res.string.students - ) + text = if (studentLazyPagingItems.itemCount > 0) + "${stringResource(Res.string.students)} (${studentLazyPagingItems.itemCount})" + else + stringResource(Res.string.students) ) }, @@ -371,9 +377,10 @@ fun ClazzDetailScreen( headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), - text = stringResource( - resource = Res.string.groups - ) + text = if (uiState.groups.isNotEmpty()) + "${stringResource(Res.string.groups)} (${uiState.groups.size})" + else + stringResource(Res.string.groups) ) }, @@ -425,14 +432,26 @@ fun ClazzDetailScreen( onClickGroup(groupData.groupId) }, leadingContent = { - Icon( - modifier = Modifier.size(40.dp).padding(8.dp), - imageVector = Icons.Filled.Groups, - contentDescription = "" - ) + Box( + modifier = Modifier.size(40.dp), + ) { + val displayMembers = groupData.memberNames.take(3) + displayMembers.forEachIndexed { i, name -> + Box( + modifier = Modifier + .offset(x = (i * 12).dp) + .zIndex((displayMembers.size - i).toFloat()) + ) { + RespectPersonAvatar( + name = name, + modifier = Modifier.size(24.dp) + ) + } + } + } }, headlineContent = { - Text(text = groupData.groupName) + Text(text = "${groupData.groupName} (${groupData.memberCount})") }, supportingContent = { Text(text = "${groupData.memberCount} ${stringResource(Res.string.students)}") diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 6be35e82e..7cc4e5d86 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -1,13 +1,15 @@ package world.respect.app.view.studentgrouping.detail import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Groups import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -17,6 +19,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.material3.Text import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import org.jetbrains.compose.resources.stringResource import world.respect.app.components.RespectBasicAlertDialog import world.respect.app.components.RespectPersonAvatar @@ -27,6 +31,7 @@ import world.respect.shared.generated.resources.delete import world.respect.shared.generated.resources.permanently_deleted import world.respect.shared.generated.resources.permanently_delete_this_group import world.respect.shared.generated.resources.student +import world.respect.shared.generated.resources.students import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailUiState import world.respect.shared.viewmodel.studentgrouping.detail.StudentGroupingDetailViewModel @@ -68,13 +73,31 @@ fun StudentGroupingDetailScreen( ListItem( modifier = Modifier.clickable {}, leadingContent = { - Icon(Icons.Default.Groups, - contentDescription = null) + Box( + modifier = Modifier.size(40.dp), + ) { + val displayMembers = uiState.groupMembers.take(3) + displayMembers.forEachIndexed { i, name -> + Box( + modifier = Modifier + .offset(x = (i * 12).dp) + .zIndex((displayMembers.size - i).toFloat()) + ) { + RespectPersonAvatar( + name = name, + modifier = Modifier.size(24.dp) + ) + } + } + } }, headlineContent = { + val label = if (uiState.groupMembers.size > 1) + stringResource(Res.string.students) + else + stringResource(Res.string.student) Text( - text = "${uiState.groupMembers.size} " + - stringResource(Res.string.student), + text = "${uiState.groupMembers.size} $label", ) } ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 621ebea64..360d87c31 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -97,6 +97,7 @@ data class GroupDisplayData( val groupId: String, val groupName: String, val memberCount: Int, + val memberNames: List = emptyList(), ) class ClazzDetailViewModel( @@ -443,7 +444,8 @@ class ClazzDetailViewModel( GroupDisplayData( groupId = group.account?.name ?: "", groupName = group.name ?: "", - memberCount = memberNames.size + memberCount = memberNames.size, + memberNames = memberNames, ) } From 18a810fa970954f664287d76525cb45d2929ef20 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 29 Apr 2026 14:02:52 +0530 Subject: [PATCH 028/130] Update ClazzDetail and StudentGroupingDetail screens to show member avatars and counts. --- .../004_001_add_groups_to_a_class_test.yaml | 46 +++++++++---------- .../composeResources/values/strings.xml | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index d878a930b..8074a005f 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -18,28 +18,28 @@ onFlowComplete: --- - runFlow: "subflows/school_admin_login_flow.yaml" - runFlow: "subflows/admin_add_class_with_members.yaml" - -- clearState: world.respect.app -- launchApp: - arguments: - respect_directory: ${output.SCHOOL_URL} -- tapOn: "Get Started" -- runFlow: - file: "subflows/get_started_select_school_by_name.yaml" - env: - SCHOOL_NAME: ${SCHOOL_NAME} -- tapOn: - id: "username" -- inputText: "teacherauser" -- tapOn: - id : "password" -- inputText: "test1234" -- tapOn: "Login" -- runFlow: - when: - visible: "Save password for Respect?" - file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" +# +#- clearState: world.respect.app +#- launchApp: +# arguments: +# respect_directory: ${output.SCHOOL_URL} +#- tapOn: "Get Started" +#- runFlow: +# file: "subflows/get_started_select_school_by_name.yaml" +# env: +# SCHOOL_NAME: ${SCHOOL_NAME} +#- tapOn: +# id: "username" +#- inputText: "teacherauser" +#- tapOn: +# id : "password" +#- inputText: "test1234" +#- tapOn: "Login" +#- runFlow: +# when: +# visible: "Save password for Respect?" +# file: "subflows/save_password_prompt_cancel.yaml" +#- assertVisible: "Apps" - tapOn: "Classes" - assertVisible: id: "app_title" @@ -70,7 +70,7 @@ onFlowComplete: - tapOn: id: "delete_group_btn" - assertVisible: "Permanently delete?" -- assertVisible: "Permanently delete this playlist" +- assertVisible: "Permanently delete this group" - assertVisible: "Cancel" - assertVisible: "Delete" - tapOn: "Cancel" diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 5cef95ca1..86d160ae7 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -461,7 +461,7 @@ Badge assigned Delete this passkey? - Permanently Delete? + Permanently delete? Permanently delete this group You could lose access to the account if you dont have another way to sign in. From c99f5b0e218156343d433ef18174095aa35c9700 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 29 Apr 2026 14:06:19 +0530 Subject: [PATCH 029/130] Update ClazzDetail and StudentGroupingDetail screens to show member avatars and counts. --- .../004_001_add_groups_to_a_class_test.yaml | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index 8074a005f..c0df83505 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -19,27 +19,27 @@ onFlowComplete: - runFlow: "subflows/school_admin_login_flow.yaml" - runFlow: "subflows/admin_add_class_with_members.yaml" # -#- clearState: world.respect.app -#- launchApp: -# arguments: -# respect_directory: ${output.SCHOOL_URL} -#- tapOn: "Get Started" -#- runFlow: -# file: "subflows/get_started_select_school_by_name.yaml" -# env: -# SCHOOL_NAME: ${SCHOOL_NAME} -#- tapOn: -# id: "username" -#- inputText: "teacherauser" -#- tapOn: -# id : "password" -#- inputText: "test1234" -#- tapOn: "Login" -#- runFlow: -# when: -# visible: "Save password for Respect?" -# file: "subflows/save_password_prompt_cancel.yaml" -#- assertVisible: "Apps" +- clearState: world.respect.app +- launchApp: + arguments: + respect_directory: ${output.SCHOOL_URL} +- tapOn: "Get Started" +- runFlow: + file: "subflows/get_started_select_school_by_name.yaml" + env: + SCHOOL_NAME: ${SCHOOL_NAME} +- tapOn: + id: "username" +- inputText: "teacherauser" +- tapOn: + id : "password" +- inputText: "test1234" +- tapOn: "Login" +- runFlow: + when: + visible: "Save password for Respect?" + file: "subflows/save_password_prompt_cancel.yaml" +- assertVisible: "Apps" - tapOn: "Classes" - assertVisible: id: "app_title" From 03302b5ac0d35426a0e5bdd29e83e0a0dcc096ba Mon Sep 17 00:00:00 2001 From: pooja Date: Wed, 29 Apr 2026 12:59:03 +0400 Subject: [PATCH 030/130] Correct the assertion text in the Maestro flow `004_001_add_groups_to_a_class_test.yaml` to verify the "Permanently delete this group" message instead of "playlist". --- .maestro/flows/004_001_add_groups_to_a_class_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index d878a930b..edf108afc 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -70,7 +70,7 @@ onFlowComplete: - tapOn: id: "delete_group_btn" - assertVisible: "Permanently delete?" -- assertVisible: "Permanently delete this playlist" +- assertVisible: "Permanently delete this group" - assertVisible: "Cancel" - assertVisible: "Delete" - tapOn: "Cancel" From 47d77de7137b929b866dff94133687f9a1930cc3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 29 Apr 2026 16:32:38 +0530 Subject: [PATCH 031/130] Update xAPI model imports and switch to `xapiStatementsResource.post` for storing statements. --- .../db/school/xapi/XapiActorDataSourceDb.kt | 4 ++-- .../school/xapi/XapiActorDataSource.kt | 3 +-- .../detail/StudentGroupingDetailViewModel.kt | 16 +++++++-------- .../edit/StudentGroupingEditViewModel.kt | 20 +++++++++---------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt index d617d4892..ad5f0f464 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt @@ -89,7 +89,7 @@ class XapiActorDataSourceDb( // Find all group member joins for this group val groupMemberJoins = schoolDb.getGroupMemberActorJoinDao() - .findByGroupActorUidList(listOf(groupActorEntity.actorUid)) + .findByGroupActorUidList(listOf(groupActorEntity.actorUid), excludeIdentifiedGroups = false) // Find all member actor entities val memberActorUids = groupMemberJoins.map { it.gmajMemberActorUid } @@ -106,7 +106,7 @@ class XapiActorDataSourceDb( groupMemberJoins = groupMemberJoins ) - return actorEntities.toModel() as? XapiGroup + return actorEntities.toModel(idOnlyFormat = false) as? XapiGroup } override suspend fun getGroupsByIds(groupIds: List): List { diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt index 69f0b0929..b893b900c 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt @@ -3,8 +3,7 @@ package world.respect.datalayer.school.xapi import kotlinx.coroutines.flow.Flow import world.respect.datalayer.DataLoadParams import world.respect.lib.xapi.model.XapiActor -import world.respect.datalayer.school.xapi.model.XapiActor -import world.respect.datalayer.school.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiGroup interface XapiActorDataSource { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 437fe0f7a..8694c35e5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -22,13 +22,7 @@ import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS -import world.respect.datalayer.school.xapi.model.VERB_DELETED -import world.respect.datalayer.school.xapi.model.XapiAccount -import world.respect.datalayer.school.xapi.model.XapiAgent -import world.respect.datalayer.school.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED -import world.respect.datalayer.school.xapi.model.XapiObjectType -import world.respect.datalayer.school.xapi.model.XapiStatement -import world.respect.datalayer.school.xapi.model.XapiVerb +import world.respect.lib.xapi.model.VERB_DELETED import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group @@ -39,6 +33,12 @@ import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel +import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiAgent +import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED +import world.respect.lib.xapi.model.XapiObjectType +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiVerb import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue import kotlin.time.Clock @@ -149,7 +149,7 @@ class StudentGroupingDetailViewModel( timestamp = Clock.System.now() ) - schoolDataSource.xapiStatementDataSource.store(listOf(statement)) + schoolDataSource.xapiStatementsResource.post(listOf(statement)) // Remove group ID from class metadata val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 0d9b5ee0e..b55f2aa5e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -27,18 +27,18 @@ import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person -import world.respect.datalayer.school.xapi.model.VERB_CREATED -import world.respect.datalayer.school.xapi.model.VERB_UPDATED -import world.respect.datalayer.school.xapi.model.XapiAccount -import world.respect.datalayer.school.xapi.model.XapiAgent -import world.respect.datalayer.school.xapi.model.XapiGroup -import world.respect.datalayer.school.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED -import world.respect.datalayer.school.xapi.model.XapiObjectType -import world.respect.datalayer.school.xapi.model.XapiStatement -import world.respect.datalayer.school.xapi.model.XapiVerb import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder +import world.respect.lib.xapi.model.VERB_CREATED +import world.respect.lib.xapi.model.VERB_UPDATED +import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiAgent +import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED +import world.respect.lib.xapi.model.XapiObjectType +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiVerb import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.account.RespectSessionAndPerson @@ -226,7 +226,7 @@ class StudentGroupingEditViewModel( timestamp = Clock.System.now() ) - schoolDataSource.xapiStatementDataSource.store(listOf(statement)) + schoolDataSource.xapiStatementsResource.post(listOf(statement)) val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch From f998ba3d302b7bc240cb08975dff02e299c7d6e2 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 4 May 2026 18:29:52 +0530 Subject: [PATCH 032/130] Refactor student grouping to use GUIDs for member identification and simplify xAPI statement logic. - Update `StudentGroupingEditViewModel` to track selected students by `Person` objects and GUIDs instead of names. - Switch xAPI verbs from `VERB_CREATED`/`VERB_UPDATED` to `VERB_SAVED`, and `VERB_DELETED` to `VERB_VOIDED`. - Enhance the xAPI statement context to include the parent class activity ID. - Remove redundant class metadata updates when saving groups, relying on the xAPI stream instead. - Update `StudentGroupingEditScreen` to verify student selection status using GUIDs. - Add a `CLASS` constant to `XapiGroup` for URI construction. --- .../edit/StudentGroupingEditScreen.kt | 2 +- .../detail/StudentGroupingDetailViewModel.kt | 6 +- .../edit/StudentGroupingEditViewModel.kt | 129 ++++++++---------- .../world/respect/lib/xapi/model/XapiGroup.kt | 1 + .../world/respect/lib/xapi/model/XapiVerb.kt | 8 +- 5 files changed, 61 insertions(+), 85 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index c8f7623fc..6b016f3ac 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -95,7 +95,7 @@ fun StudentGroupingEditScreen( } ) { student -> - val isSelected = student?.fullName() in uiState.selectedStudentNames + val isSelected = student?.guid in uiState.selectedStudentIds PersonListItemWithMenu( person = student, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 8694c35e5..d9b4b709b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -22,7 +22,7 @@ import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS -import world.respect.lib.xapi.model.VERB_DELETED +import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_group @@ -138,8 +138,8 @@ class StudentGroupingDetailViewModel( ) val verb = XapiVerb( - id = VERB_DELETED, - display = mapOf("en" to VERB_DELETED) + id = VERB_VOIDED, + display = mapOf("en" to VERB_VOIDED) ) val statement = XapiStatement( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index b55f2aa5e..04208f385 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -8,33 +8,27 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.putJsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.DataLoadParams -import world.respect.datalayer.DataLoadState -import world.respect.datalayer.DataLoadingState import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource -import world.respect.datalayer.school.model.Clazz -import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder -import world.respect.lib.xapi.model.VERB_CREATED -import world.respect.lib.xapi.model.VERB_UPDATED +import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiAgent +import world.respect.lib.xapi.model.XapiContext +import world.respect.lib.xapi.model.XapiContextActivities import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiObjectType import world.respect.lib.xapi.model.XapiStatement @@ -65,16 +59,21 @@ data class StudentGroupingEditUiState( val groupName: String = "", val groupNameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val selectedStudentIds: List = emptyList(), - val selectedStudentNames: List = emptyList(), - val personName: String = "", - val clazz: DataLoadState = DataLoadingState(), + val selectedStudents: List = emptyList() +) { + val personName: String + get() = selectedAccount?.person?.fullName() ?: "" - ) + val personId: String + get() = selectedAccount?.person?.guid ?: "" + + val selectedStudentIds: Set + get() = selectedStudents.map { it.guid }.toSet() +} class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, - var respectAccountManager: RespectAccountManager, + respectAccountManager: RespectAccountManager ) : RespectViewModel(savedStateHandle), KoinScopeComponent { @@ -86,6 +85,9 @@ class StudentGroupingEditViewModel( private val schoolDataSource: SchoolDataSource by inject() private val route: StudentGroupingEdit = savedStateHandle.toRoute() + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + + private fun pagingSourceByRole(): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { schoolDataSource.personDataSource.listAsPagingSource( @@ -128,15 +130,22 @@ class StudentGroupingEditViewModel( val group = schoolDataSource.xapiActorDataSource.getGroupDetail(groupId) ?: return@launch - val memberNames = group.member - ?.mapNotNull { it.name } + val memberIds = group.member + ?.mapNotNull { it.account?.name } ?: emptyList() + val persons = memberIds.mapNotNull { id -> + schoolDataSource.personDataSource.findByGuid( + DataLoadParams(), + id + ).dataOrNull() + } + // ✅ Step 3: update state _uiState.update { prev -> prev.copy( groupName = group.name ?: "", - selectedStudentNames = memberNames, + selectedStudents = persons ) } @@ -145,30 +154,21 @@ class StudentGroupingEditViewModel( } } } - viewModelScope.launch { respectAccountManager.selectedAccountAndPersonFlow.collect { selectedAccount -> _uiState.update { - it.copy( - personName = selectedAccount?.person?.fullName() ?: "" - ) + it.copy(selectedAccount = selectedAccount) } } } - - viewModelScope.launch { - schoolDataSource.classDataSource.findByGuidAsFlow(route.classUid) - .collect { clazz -> - _uiState.update { it.copy(clazz = clazz) } - } - } } @OptIn(ExperimentalUuidApi::class) - fun onClickSave() { + fun onClickSave() + { - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self val groupName = _uiState.value.groupName + val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" if (groupName.isBlank()) { _uiState.update { @@ -181,12 +181,12 @@ class StudentGroupingEditViewModel( viewModelScope.launch { try { - val members = _uiState.value.selectedStudentNames.map { studentName -> + val members = _uiState.value.selectedStudents.map { person -> XapiAgent( - name = studentName, + name = person.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( - name = studentName, + name = person.guid, homePage = schoolSelfUrl.toString() ) ) @@ -208,12 +208,12 @@ class StudentGroupingEditViewModel( name = _uiState.value.personName, objectType = XapiObjectType.Agent, account = XapiAccount( - name = respectAccountManager.activeAccount?.userGuid ?: "", + name = _uiState.value.personId, homePage = schoolSelfUrl.toString() ) ) - val verbId = if (route.groupId != null) VERB_UPDATED else VERB_CREATED + val verbId = VERB_SAVED val verb = XapiVerb( id = verbId, display = mapOf("en" to verbId) @@ -223,40 +223,21 @@ class StudentGroupingEditViewModel( actor = actor, verb = verb, `object` = group, - timestamp = Clock.System.now() + timestamp = Clock.System.now(), + context = XapiContext( + contextActivities = XapiContextActivities( + parent = listOf( + XapiActivity( + id = classActivityId, + objectType = XapiObjectType.Activity + ) + ) + ) + ) ) schoolDataSource.xapiStatementsResource.post(listOf(statement)) - val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch - - val existingGroupIds = clazz.metadata - ?.get(GROUP_IDS) - ?.jsonArray - ?.map { it.jsonPrimitive.content } - ?.toMutableList() - ?: mutableListOf() - - if (route.groupId == null && !existingGroupIds.contains(groupId)) { - existingGroupIds.add(groupId) - } - - val updatedMetadata = buildJsonObject { - clazz.metadata?.forEach { (key, value) -> - if (key != GROUP_IDS) put(key, value) - } - putJsonArray(GROUP_IDS) { - existingGroupIds.forEach { add(it) } - } - } - - val updatedClazz = clazz.copy( - metadata = updatedMetadata, - lastModified = Clock.System.now() - ) - - schoolDataSource.classDataSource.store(listOf(updatedClazz)) - if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -277,16 +258,14 @@ class StudentGroupingEditViewModel( fun onStudentCheckedChange(person: Person, isChecked: Boolean) { _uiState.update { prev -> - val name = person.fullName() - val updatedNames = if (isChecked) { - prev.selectedStudentNames + name + + val updated = if (isChecked) { + prev.selectedStudents + person } else { - prev.selectedStudentNames - name + prev.selectedStudents.filterNot { it.guid == person.guid } } - prev.copy( - selectedStudentNames = updatedNames - ) + prev.copy(selectedStudents = updated) } } diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt index fde934daa..5691da1a5 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt @@ -31,6 +31,7 @@ data class XapiGroup( companion object { const val RESULT_KEY_GROUP_UPDATED = "groupUpdated" + const val CLASS = "class/" } val isIdentified: Boolean = !isAnonymous diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index c95232d81..6fd1e8313 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -6,12 +6,8 @@ import kotlinx.serialization.Serializable const val VERB_COMPLETED = "http://adlnet.gov/expapi/verbs/completed" const val VERB_PROGRESSED = "http://adlnet.gov/expapi/verbs/progressed" - -const val VERB_CREATED = "http://adlnet.gov/expapi/verbs/create" - -const val VERB_UPDATED = "http://adlnet.gov/expapi/verbs/update" - -const val VERB_DELETED = "http://adlnet.gov/expapi/verbs/deleted" +const val VERB_SAVED = "http://adlnet.gov/expapi/verbs/saved" +const val VERB_VOIDED = "http://adlnet.gov/expapi/verbs/voided" @Serializable data class XapiVerb( From 7ba9aa21334d551eaae4d92c89e51123cef393d5 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 4 May 2026 22:40:33 +0530 Subject: [PATCH 033/130] Update `ClazzDetailViewModel` to fetch student groups from xAPI statements instead of metadata and fix a column name in `XapiStatementEntityDao` queries(The statementVerbId is a String (the URL), and statementVerbUid is a Long (the hashed UID). The query is comparing statementVerbId (String) with :verbUid parameter (which is being passed as a Long from the hash).) --- .../xapi/daos/XapiStatementEntityDao.kt | 2 +- .../xapi/daos/XapiStatementEntityJsonDao.kt | 2 +- .../clazz/detail/ClazzDetailViewModel.kt | 63 +++++++++---------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt index ad78d754b..680fdde9b 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt @@ -87,7 +87,7 @@ interface XapiStatementEntityDao { ) ) ) - AND (:verbUid = 0 OR XapiStatementEntity.statementVerbId = :verbUid) + AND (:verbUid = 0 OR XapiStatementEntity.statementVerbUid = :verbUid) -- Handle activity parameter AND ( :activityUid = 0 OR ( XapiStatementEntity.statementObjectType = ${XapiEntityObjectTypeFlags.ACTIVITY} diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt index 513cb9b4f..ae7eb08a9 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt @@ -79,7 +79,7 @@ interface XapiStatementEntityJsonDao { ) ) ) - AND (:verbUid = 0 OR XapiStatementEntity.statementVerbId = :verbUid) + AND (:verbUid = 0 OR XapiStatementEntity.statementVerbUid = :verbUid) -- Handle activity parameter AND ( :activityUid = 0 OR ( XapiStatementEntity.statementObjectType = ${XapiEntityObjectTypeFlags.ACTIVITY} diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 360d87c31..927e093ee 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import kotlinx.serialization.json.JsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope @@ -65,6 +64,11 @@ import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL import kotlin.getValue import kotlin.time.Clock +import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.resources.XapiStatementsResource +import world.respect.lib.xapi.XapiRequestHeaders +import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS data class ClazzDetailUiState( val teachers: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -121,6 +125,9 @@ class ClazzDetailViewModel( private val route: ClazzDetail = savedStateHandle.toRoute() + val schoolSelfUrl = accountManager.activeAccount?.school?.self?.toString() + val classActivityId = "${schoolSelfUrl}${CLASS}${route.guid}" + private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { schoolDataSource.personDataSource.listAsPagingSource( @@ -187,26 +194,12 @@ class ClazzDetailViewModel( it.copy(title = clazz.dataOrNull()?.title?.asUiText()) } - // Extract group IDs from metadata - val clazzData = clazz.dataOrNull() - val groupIds = if (clazzData != null) { - extractGroupIdsFromMetadata(clazzData) - } else { - emptyList() - } - _uiState.update { - it.copy( - clazz = clazz, - groupIds = groupIds - ) + it.copy(clazz = clazz) } - if (groupIds.isNotEmpty()) { - loadGroupDetails(groupIds) - } else { - _uiState.update { it.copy(groups = emptyList()) } - } + // Load groups from xAPI statements + loadGroupsFromXapi() } } @@ -421,21 +414,26 @@ class ClazzDetailViewModel( ) } - private fun extractGroupIdsFromMetadata(clazz: Clazz): List { - return try { - val metadata = clazz.metadata ?: return emptyList() - val groupIdsJsonArray = metadata["groupIds"] as? JsonArray - groupIdsJsonArray?.map { it.toString().trim('"') } ?: emptyList() - } catch (e: Throwable) { - Napier.w("Failed to extract group IDs from metadata", throwable = e) - emptyList() - } - } - - private fun loadGroupDetails(groupIds: List) { + @OptIn(kotlin.uuid.ExperimentalUuidApi::class) + private fun loadGroupsFromXapi() { viewModelScope.launch { try { - val groups = schoolDataSource.xapiActorDataSource.getGroupsByIds(groupIds) + + val response = schoolDataSource.xapiStatementsResource.get( + XapiStatementsResource.GetStatementsRequest( + params = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + headers = XapiRequestHeaders() + ) + ) + + val groups = response.statementResult.statements.mapNotNull { statement -> + statement.`object` as? XapiGroup + }.distinctBy { it.account?.name } + val groupDisplayDataList = groups.map { group -> val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() Napier.i("Loaded Group: ${group.name}") @@ -453,7 +451,8 @@ class ClazzDetailViewModel( prev.copy(groups = groupDisplayDataList) } } catch (e: Throwable) { - Napier.e("loadGroupDetails ERROR", throwable = e) + Napier.e("loadGroupsFromXapi ERROR", throwable = e) + _uiState.update { it.copy(groups = emptyList()) } } } } From be19ae825c6ce4d1fa1a0ad725bfe77215efa92b Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 5 May 2026 11:29:42 +0530 Subject: [PATCH 034/130] Refactor group data fetching to use xAPI statements via flows across `ClazzDetail`, `StudentGroupingDetail`, and `StudentGroupingEdit` view models. - Update `ClazzDetailViewModel` to observe groups from xAPI statements as a flow using `observeGroupsFromXapi`. - Update `StudentGroupingDetailViewModel` and `StudentGroupingEditViewModel` to retrieve group details by filtering statements associated with a specific class activity ID. - Remove deprecated group-related methods from `XapiActorDataSourceDb`. - Adjust `StudentGroupingDetailUiState` to remove the direct `Clazz` dependency. - Standardize on `DataLoadParams` from `world.respect.lib.dataloadstate`. --- .../db/school/xapi/XapiActorDataSourceDb.kt | 43 --------- .../clazz/detail/ClazzDetailViewModel.kt | 36 ++++---- .../detail/StudentGroupingDetailViewModel.kt | 91 +++++++++---------- .../edit/StudentGroupingEditViewModel.kt | 23 +++-- 4 files changed, 79 insertions(+), 114 deletions(-) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt index f5ef1841c..4f0d87e7e 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/XapiActorDataSourceDb.kt @@ -1,13 +1,9 @@ package world.respect.datalayer.db.school.xapi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import world.respect.datalayer.AuthenticatedUserPrincipalId import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectSchoolDatabase -import world.respect.datalayer.db.school.xapi.adapters.ActorEntities import world.respect.datalayer.db.school.xapi.adapters.toEntities -import world.respect.datalayer.db.school.xapi.adapters.toModel import world.respect.datalayer.ext.EPOCH import world.respect.datalayer.school.xapi.XapiActorDataSourceLocal import world.respect.lib.xapi.XapiRequestHeaders @@ -81,43 +77,4 @@ class XapiActorDataSourceDb( ) { TODO("Not yet implemented") } - - override suspend fun getGroupDetail(groupId: String): XapiGroup? { - // Find the group actor by its accountName (groupId) - val groupActorEntity = schoolDb.getActorDao().findGroupByAccountNameAsync(groupId) - ?: return null - - // Find all group member joins for this group - val groupMemberJoins = schoolDb.getGroupMemberActorJoinDao() - .findByGroupActorUidList(listOf(groupActorEntity.actorUid), excludeIdentifiedGroups = false) - - // Find all member actor entities - val memberActorUids = groupMemberJoins.map { it.gmajMemberActorUid } - val memberActorEntities = if (memberActorUids.isNotEmpty()) { - schoolDb.getActorDao().findByUidList(memberActorUids) - } else { - emptyList() - } - - // Convert to model using adapter - val actorEntities = ActorEntities( - actor = groupActorEntity, - groupMemberAgents = memberActorEntities, - groupMemberJoins = groupMemberJoins - ) - - return actorEntities.toModel(idOnlyFormat = false) as? XapiGroup - } - - override suspend fun getGroupsByIds(groupIds: List): List { - if (groupIds.isEmpty()) return emptyList() - - return groupIds.mapNotNull { groupId -> - getGroupDetail(groupId) - } - } - - override fun getGroupDetailAsFlow(groupId: String): Flow = flow { - emit(getGroupDetail(groupId)) - } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 23622ada6..f7c350d77 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -67,7 +67,6 @@ import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.resources.XapiStatementsResource -import world.respect.lib.xapi.XapiRequestHeaders import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS data class ClazzDetailUiState( @@ -197,12 +196,14 @@ class ClazzDetailViewModel( _uiState.update { it.copy(clazz = clazz) } - - // Load groups from xAPI statements - loadGroupsFromXapi() } } + // Observe groups from xAPI statements as a flow + viewModelScope.launch { + observeGroupsFromXapi() + } + viewModelScope.launch { _uiState.whenSubscribed { accountManager.selectedAccountAndPersonFlow.collect { selectedAccountAndPerson -> @@ -415,22 +416,19 @@ class ClazzDetailViewModel( } @OptIn(kotlin.uuid.ExperimentalUuidApi::class) - private fun loadGroupsFromXapi() { - viewModelScope.launch { + private suspend fun observeGroupsFromXapi() { + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).collect { dataLoadState -> try { + val statementResult = dataLoadState.dataOrNull() ?: return@collect - val response = schoolDataSource.xapiStatementsResource.get( - XapiStatementsResource.GetStatementsRequest( - params = XapiStatementsResource.GetStatementParams( - verb = VERB_SAVED, - activity = classActivityId, - relatedActivities = true, - ), - headers = XapiRequestHeaders() - ) - ) - - val groups = response.statementResult.statements.mapNotNull { statement -> + val groups = statementResult.statements.mapNotNull { statement -> statement.`object` as? XapiGroup }.distinctBy { it.account?.name } @@ -451,7 +449,7 @@ class ClazzDetailViewModel( prev.copy(groups = groupDisplayDataList) } } catch (e: Throwable) { - Napier.e("loadGroupsFromXapi ERROR", throwable = e) + Napier.e("observeGroupsFromXapi ERROR", throwable = e) _uiState.update { it.copy(groups = emptyList()) } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index d9b4b709b..73dc6b80d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -8,24 +8,17 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.putJsonArray import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope -import world.respect.datalayer.DataLoadState -import world.respect.datalayer.DataLoadingState import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.ext.dataOrNull -import world.respect.datalayer.school.model.Clazz -import world.respect.datalayer.school.model.Clazz.Companion.GROUP_IDS -import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.dataloadstate.DataLoadParams +import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.edit import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner @@ -33,15 +26,10 @@ import world.respect.shared.navigation.StudentGroupingDetail import world.respect.shared.navigation.StudentGroupingEdit import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel -import world.respect.lib.xapi.model.XapiAccount -import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED -import world.respect.lib.xapi.model.XapiObjectType -import world.respect.lib.xapi.model.XapiStatement -import world.respect.lib.xapi.model.XapiVerb +import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue -import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi @@ -49,12 +37,11 @@ data class StudentGroupingDetailUiState( val groupName: String = "", val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, - val clazz: DataLoadState = DataLoadingState(), ) class StudentGroupingDetailViewModel( savedStateHandle: SavedStateHandle, - var respectAccountManager: RespectAccountManager, + private val respectAccountManager: RespectAccountManager, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = respectAccountManager.requireActiveAccountScope() @@ -67,6 +54,9 @@ class StudentGroupingDetailViewModel( private val route: StudentGroupingDetail = savedStateHandle.toRoute() + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self?.toString() + val classActivityId = "${schoolSelfUrl}${CLASS}${route.classId}" + init { _appUiState.update { it.copy( @@ -80,7 +70,6 @@ class StudentGroupingDetailViewModel( } loadGroupDetail() - // Listen for result from edit screen and refresh val navResultReturner: NavResultReturner = getKoin().get() viewModelScope.launch { navResultReturner.filteredResultFlowForKey( @@ -89,13 +78,6 @@ class StudentGroupingDetailViewModel( loadGroupDetail() } } - - viewModelScope.launch { - schoolDataSource.classDataSource.findByGuidAsFlow(route.classId) - .collect { clazz -> - _uiState.update { it.copy(clazz = clazz) } - } - } } fun onClickEdit() { @@ -123,6 +105,7 @@ class StudentGroupingDetailViewModel( viewModelScope.launch { try { +/* val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self val group = schoolDataSource.xapiActorDataSource.getGroupDetail(route.groupId) @@ -174,8 +157,9 @@ class StudentGroupingDetailViewModel( metadata = updatedMetadata, lastModified = Clock.System.now() ) +*/ - schoolDataSource.classDataSource.store(listOf(updatedClazz)) + // schoolDataSource.classDataSource.store(listOf(updatedClazz)) _navCommandFlow.tryEmit(NavCommand.PopUp()) @@ -185,29 +169,44 @@ class StudentGroupingDetailViewModel( } } + @OptIn(ExperimentalUuidApi::class) private fun loadGroupDetail() { viewModelScope.launch { try { - val group = schoolDataSource.xapiActorDataSource.getGroupDetail(route.groupId) - - if (group != null) { - val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - - _uiState.update { prev -> - prev.copy( - groupName = group.name ?: "", - groupMembers = memberNames - ) + // Fetch statements from xAPI using the classActivity as parent context + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).collect { dataLoadState -> + val statementResult = dataLoadState.dataOrNull() ?: return@collect + + val group = statementResult.statements + .mapNotNull { statement -> statement.`object` as? XapiGroup } + .firstOrNull { it.account?.name == route.groupId } + + if (group != null) { + val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + + _uiState.update { prev -> + prev.copy( + groupName = group.name ?: "", + groupMembers = memberNames + ) + } + + _appUiState.update { + it.copy( + title = group.name?.asUiText() ?: "".asUiText(), + userAccountIconVisible = false, + hideBottomNavigation = true, + ) + } } } - - _appUiState.update { - it.copy( - title = group?.name?.asUiText() ?: Res.string.create_group.asUiText(), - userAccountIconVisible = false, - hideBottomNavigation = true, - ) - } } catch (e: Throwable) { Napier.e("loadGroupDetail ERROR", throwable = e) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 04208f385..59ee7f28c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope -import world.respect.datalayer.DataLoadParams import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.ext.dataOrNull @@ -21,6 +20,7 @@ import world.respect.datalayer.school.model.Person import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder +import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity @@ -33,6 +33,7 @@ import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiObjectType import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.model.XapiVerb +import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.account.RespectSessionAndPerson @@ -86,7 +87,7 @@ class StudentGroupingEditViewModel( private val route: StudentGroupingEdit = savedStateHandle.toRoute() val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self - + val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" private fun pagingSourceByRole(): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { @@ -125,9 +126,21 @@ class StudentGroupingEditViewModel( it.copy(students = studentPagingSource) } route.groupId?.let { groupId -> + @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { try { - val group = schoolDataSource.xapiActorDataSource.getGroupDetail(groupId) + val statementResult = schoolDataSource.xapiStatementsResource.get( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull() ?: return@launch + + val group = statementResult.statements + .mapNotNull { statement -> statement.`object` as? XapiGroup } + .firstOrNull { it.account?.name == groupId } ?: return@launch val memberIds = group.member @@ -141,7 +154,6 @@ class StudentGroupingEditViewModel( ).dataOrNull() } - // ✅ Step 3: update state _uiState.update { prev -> prev.copy( groupName = group.name ?: "", @@ -164,8 +176,7 @@ class StudentGroupingEditViewModel( } @OptIn(ExperimentalUuidApi::class) - fun onClickSave() - { + fun onClickSave() { val groupName = _uiState.value.groupName val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" From c81f70d7dffbc618b1a544ffa995be1c2cded021 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 5 May 2026 12:55:39 +0530 Subject: [PATCH 035/130] Implement group deletion by voiding the corresponding xAPI statement in `StudentGroupingDetailViewModel`. - Add `statementId` to `StudentGroupingDetailUiState`. - Update `onConfirmDeleteGroup` to post a `VERB_VOIDED` statement referencing the original group statement. - Update data loading logic to filter out voided statements when fetching the group detail. - Remove obsolete debug logs in `ClazzDetailViewModel`. --- .../clazz/detail/ClazzDetailViewModel.kt | 6 +- .../detail/StudentGroupingDetailViewModel.kt | 94 +++++++++++-------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index f7c350d77..7a13f881b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -62,10 +62,11 @@ import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.app.appstate.Snack import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL -import kotlin.getValue import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiStatementRef import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS @@ -378,7 +379,6 @@ class ClazzDetailViewModel( schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) } catch (e: Throwable) { - //do something Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) } @@ -434,8 +434,6 @@ class ClazzDetailViewModel( val groupDisplayDataList = groups.map { group -> val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - Napier.i("Loaded Group: ${group.name}") - Napier.i("Group Members: ${memberNames.joinToString(", ")}") GroupDisplayData( groupId = group.account?.name ?: "", diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 73dc6b80d..27936b1b7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -15,7 +15,14 @@ import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.ext.dataOrNull import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiObjectType +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiStatementRef +import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res @@ -30,6 +37,7 @@ import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue +import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi @@ -37,6 +45,7 @@ data class StudentGroupingDetailUiState( val groupName: String = "", val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, + val statementId: String? = null, ) class StudentGroupingDetailViewModel( @@ -105,11 +114,13 @@ class StudentGroupingDetailViewModel( viewModelScope.launch { try { -/* - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + val statementId = _uiState.value.statementId + if (statementId == null) { + Napier.e("onConfirmDeleteGroup: No statement ID available") + return@launch + } - val group = schoolDataSource.xapiActorDataSource.getGroupDetail(route.groupId) - ?: return@launch + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self val actor = XapiAgent( name = respectAccountManager.activeAccount?.userGuid ?: "", @@ -122,44 +133,23 @@ class StudentGroupingDetailViewModel( val verb = XapiVerb( id = VERB_VOIDED, - display = mapOf("en" to VERB_VOIDED) + display = mapOf("en" to "voided") + ) + + // Use StatementRef to reference the statement being voided + val statementRef = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = statementId ) - val statement = XapiStatement( + val voidingStatement = XapiStatement( actor = actor, verb = verb, - `object` = group, + `object` = statementRef, timestamp = Clock.System.now() ) - schoolDataSource.xapiStatementsResource.post(listOf(statement)) - - // Remove group ID from class metadata - val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch - - val existingGroupIds = clazz.metadata - ?.get(GROUP_IDS) - ?.jsonArray - ?.map { it.jsonPrimitive.content } - ?.filter { it != route.groupId } - ?: emptyList() - - val updatedMetadata = buildJsonObject { - clazz.metadata?.forEach { (key, value) -> - if (key != GROUP_IDS) put(key, value) - } - putJsonArray(GROUP_IDS) { - existingGroupIds.forEach { add(it) } - } - } - - val updatedClazz = clazz.copy( - metadata = updatedMetadata, - lastModified = Clock.System.now() - ) -*/ - - // schoolDataSource.classDataSource.store(listOf(updatedClazz)) + schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) _navCommandFlow.tryEmit(NavCommand.PopUp()) @@ -184,17 +174,41 @@ class StudentGroupingDetailViewModel( ).collect { dataLoadState -> val statementResult = dataLoadState.dataOrNull() ?: return@collect - val group = statementResult.statements - .mapNotNull { statement -> statement.`object` as? XapiGroup } - .firstOrNull { it.account?.name == route.groupId } + // Get all voiding statements to filter out voided statements + val voidingStatements = schoolDataSource.xapiStatementsResource.get( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_VOIDED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull()?.statements ?: emptyList() + + // Extract IDs of voided statements + val voidedStatementIds = voidingStatements + .mapNotNull { it.`object` as? XapiStatementRef } + .map { it.id } + .toSet() + + // Filter out voided statements and find the group + val groupStatement = statementResult.statements + .filter { statement -> + statement.id?.toString() !in voidedStatementIds + } + .firstOrNull { statement -> + val group = statement.`object` as? XapiGroup + group?.account?.name == route.groupId + } - if (group != null) { + if (groupStatement != null) { + val group = groupStatement.`object` as XapiGroup val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() _uiState.update { prev -> prev.copy( groupName = group.name ?: "", - groupMembers = memberNames + groupMembers = memberNames, + statementId = groupStatement.id?.toString() ) } From b76f911a932971cca634461f9dd0ce98861eb5de Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 5 May 2026 13:12:09 +0530 Subject: [PATCH 036/130] Implement logic in `StudentGroupingDetailViewModel` to dynamically find and void the correct xAPI statement when deleting a group by matching the `groupId`. --- .../detail/StudentGroupingDetailViewModel.kt | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 27936b1b7..43c4e40cb 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -114,9 +114,55 @@ class StudentGroupingDetailViewModel( viewModelScope.launch { try { - val statementId = _uiState.value.statementId + // Fetch statements to find the one matching route.groupId + val statementResult = schoolDataSource.xapiStatementsResource.get( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull() + + if (statementResult == null) { + Napier.w("onConfirmDeleteGroup: No statements found") + return@launch + } + + // Get all voiding statements to filter out already voided statements + val voidingStatements = schoolDataSource.xapiStatementsResource.get( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_VOIDED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull()?.statements ?: emptyList() + + // Extract IDs of voided statements + val voidedStatementIds = voidingStatements + .mapNotNull { it.`object` as? XapiStatementRef } + .map { it.id } + .toSet() + + // Find the statement where group.account.name matches route.groupId + val statementToVoid = statementResult.statements + .filter { statement -> + statement.id?.toString() !in voidedStatementIds + } + .firstOrNull { statement -> + val group = statement.`object` as? XapiGroup + group?.account?.name == route.groupId + } + + if (statementToVoid == null) { + Napier.w("onConfirmDeleteGroup: Statement with groupId ${route.groupId} not found") + return@launch + } + + val statementId = statementToVoid.id?.toString() if (statementId == null) { - Napier.e("onConfirmDeleteGroup: No statement ID available") + Napier.w("onConfirmDeleteGroup: Statement has no ID") return@launch } @@ -163,7 +209,6 @@ class StudentGroupingDetailViewModel( private fun loadGroupDetail() { viewModelScope.launch { try { - // Fetch statements from xAPI using the classActivity as parent context schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, @@ -174,27 +219,7 @@ class StudentGroupingDetailViewModel( ).collect { dataLoadState -> val statementResult = dataLoadState.dataOrNull() ?: return@collect - // Get all voiding statements to filter out voided statements - val voidingStatements = schoolDataSource.xapiStatementsResource.get( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_VOIDED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull()?.statements ?: emptyList() - - // Extract IDs of voided statements - val voidedStatementIds = voidingStatements - .mapNotNull { it.`object` as? XapiStatementRef } - .map { it.id } - .toSet() - - // Filter out voided statements and find the group val groupStatement = statementResult.statements - .filter { statement -> - statement.id?.toString() !in voidedStatementIds - } .firstOrNull { statement -> val group = statement.`object` as? XapiGroup group?.account?.name == route.groupId From b79334ddaf924bd794ad7d7c5cd1dd37966debcc Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 5 May 2026 13:27:57 +0530 Subject: [PATCH 037/130] Update group deletion logic to use `statementGroupId` from UI state and implement voided statement filtering in `ClazzDetailViewModel`. --- .../clazz/detail/ClazzDetailViewModel.kt | 21 ++++++ .../detail/StudentGroupingDetailViewModel.kt | 64 +++---------------- 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 7a13f881b..a3fbfd6fb 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -417,6 +417,27 @@ class ClazzDetailViewModel( @OptIn(kotlin.uuid.ExperimentalUuidApi::class) private suspend fun observeGroupsFromXapi() { + // First, get all voiding statements to filter out voided statements + val voidingStatements = try { + schoolDataSource.xapiStatementsResource.get( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_VOIDED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull()?.statements ?: emptyList() + } catch (e: Throwable) { + Napier.w("Failed to fetch voiding statements", e) + emptyList() + } + + // Extract IDs of voided statements + val voidedStatementIds = voidingStatements + .mapNotNull { it.`object` as? XapiStatementRef } + .map { it.id } + .toSet() + schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 43c4e40cb..4f7e57ead 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -45,7 +45,7 @@ data class StudentGroupingDetailUiState( val groupName: String = "", val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, - val statementId: String? = null, + val statementGroupId: String? = null, ) class StudentGroupingDetailViewModel( @@ -114,55 +114,9 @@ class StudentGroupingDetailViewModel( viewModelScope.launch { try { - // Fetch statements to find the one matching route.groupId - val statementResult = schoolDataSource.xapiStatementsResource.get( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_SAVED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull() - - if (statementResult == null) { - Napier.w("onConfirmDeleteGroup: No statements found") - return@launch - } - - // Get all voiding statements to filter out already voided statements - val voidingStatements = schoolDataSource.xapiStatementsResource.get( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_VOIDED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull()?.statements ?: emptyList() - - // Extract IDs of voided statements - val voidedStatementIds = voidingStatements - .mapNotNull { it.`object` as? XapiStatementRef } - .map { it.id } - .toSet() - - // Find the statement where group.account.name matches route.groupId - val statementToVoid = statementResult.statements - .filter { statement -> - statement.id?.toString() !in voidedStatementIds - } - .firstOrNull { statement -> - val group = statement.`object` as? XapiGroup - group?.account?.name == route.groupId - } - - if (statementToVoid == null) { - Napier.w("onConfirmDeleteGroup: Statement with groupId ${route.groupId} not found") - return@launch - } - - val statementId = statementToVoid.id?.toString() - if (statementId == null) { - Napier.w("onConfirmDeleteGroup: Statement has no ID") + val statementGroupId = _uiState.value.statementGroupId + if (statementGroupId == null) { + Napier.w("onConfirmDeleteGroup: Statement ID not found") return@launch } @@ -179,20 +133,20 @@ class StudentGroupingDetailViewModel( val verb = XapiVerb( id = VERB_VOIDED, - display = mapOf("en" to "voided") + display = mapOf("en" to VERB_VOIDED) ) - // Use StatementRef to reference the statement being voided val statementRef = XapiStatementRef( objectType = XapiObjectType.StatementRef, - id = statementId + id = statementGroupId ) val voidingStatement = XapiStatement( actor = actor, verb = verb, `object` = statementRef, - timestamp = Clock.System.now() + timestamp = Clock.System.now(), + stored = Clock.System.now() ) schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) @@ -233,7 +187,7 @@ class StudentGroupingDetailViewModel( prev.copy( groupName = group.name ?: "", groupMembers = memberNames, - statementId = groupStatement.id?.toString() + statementGroupId = groupStatement.id?.toString() ) } From 1465c6ea4e58065d45ed4185d6663288b9ca61ec Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 5 May 2026 18:08:08 +0530 Subject: [PATCH 038/130] Simplify xAPI statement handling by removing manual voided statement filtering in `ClazzDetailViewModel` and stripping redundant try-catch blocks and UI state updates in student grouping ViewModels. --- .../clazz/detail/ClazzDetailViewModel.kt | 29 +------- .../detail/StudentGroupingDetailViewModel.kt | 66 +++++++++---------- .../edit/StudentGroupingEditViewModel.kt | 10 +-- .../world/respect/lib/xapi/model/XapiVerb.kt | 1 + 4 files changed, 33 insertions(+), 73 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index a3fbfd6fb..f43e74af0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -64,9 +64,7 @@ import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED -import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.lib.xapi.model.XapiGroup -import world.respect.lib.xapi.model.XapiStatementRef import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS @@ -417,27 +415,6 @@ class ClazzDetailViewModel( @OptIn(kotlin.uuid.ExperimentalUuidApi::class) private suspend fun observeGroupsFromXapi() { - // First, get all voiding statements to filter out voided statements - val voidingStatements = try { - schoolDataSource.xapiStatementsResource.get( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_VOIDED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull()?.statements ?: emptyList() - } catch (e: Throwable) { - Napier.w("Failed to fetch voiding statements", e) - emptyList() - } - - // Extract IDs of voided statements - val voidedStatementIds = voidingStatements - .mapNotNull { it.`object` as? XapiStatementRef } - .map { it.id } - .toSet() - schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, @@ -446,7 +423,7 @@ class ClazzDetailViewModel( ), dataLoadParams = DataLoadParams() ).collect { dataLoadState -> - try { + val statementResult = dataLoadState.dataOrNull() ?: return@collect val groups = statementResult.statements.mapNotNull { statement -> @@ -467,10 +444,6 @@ class ClazzDetailViewModel( _uiState.update { prev -> prev.copy(groups = groupDisplayDataList) } - } catch (e: Throwable) { - Napier.e("observeGroupsFromXapi ERROR", throwable = e) - _uiState.update { it.copy(groups = emptyList()) } - } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 4f7e57ead..2e9a9c46f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -113,49 +113,45 @@ class StudentGroupingDetailViewModel( _uiState.update { it.copy(showDeleteGroupDialog = false) } viewModelScope.launch { - try { - val statementGroupId = _uiState.value.statementGroupId - if (statementGroupId == null) { - Napier.w("onConfirmDeleteGroup: Statement ID not found") - return@launch - } + val statementGroupId = _uiState.value.statementGroupId + if (statementGroupId == null) { + Napier.w("onConfirmDeleteGroup: Statement ID not found") + return@launch + } - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self - val actor = XapiAgent( + val actor = XapiAgent( + name = respectAccountManager.activeAccount?.userGuid ?: "", + objectType = XapiObjectType.Agent, + account = XapiAccount( name = respectAccountManager.activeAccount?.userGuid ?: "", - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = respectAccountManager.activeAccount?.userGuid ?: "", - homePage = schoolSelfUrl.toString() - ) + homePage = schoolSelfUrl.toString() ) + ) - val verb = XapiVerb( - id = VERB_VOIDED, - display = mapOf("en" to VERB_VOIDED) - ) + val verb = XapiVerb( + id = VERB_VOIDED, + display = mapOf("en" to VERB_VOIDED) + ) - val statementRef = XapiStatementRef( - objectType = XapiObjectType.StatementRef, - id = statementGroupId - ) + val statementRef = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = statementGroupId + ) - val voidingStatement = XapiStatement( - actor = actor, - verb = verb, - `object` = statementRef, - timestamp = Clock.System.now(), - stored = Clock.System.now() - ) + val voidingStatement = XapiStatement( + actor = actor, + verb = verb, + `object` = statementRef, + timestamp = Clock.System.now(), + stored = Clock.System.now() + ) - schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) - _navCommandFlow.tryEmit(NavCommand.PopUp()) + _navCommandFlow.tryEmit(NavCommand.PopUp()) - } catch (e: Throwable) { - Napier.e("onConfirmDeleteGroup ERROR", throwable = e) - } } } @@ -193,9 +189,7 @@ class StudentGroupingDetailViewModel( _appUiState.update { it.copy( - title = group.name?.asUiText() ?: "".asUiText(), - userAccountIconVisible = false, - hideBottomNavigation = true, + title = group.name?.asUiText() ?: "".asUiText() ) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 59ee7f28c..a0574ca94 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -128,7 +128,7 @@ class StudentGroupingEditViewModel( route.groupId?.let { groupId -> @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { - try { + val statementResult = schoolDataSource.xapiStatementsResource.get( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, @@ -160,10 +160,6 @@ class StudentGroupingEditViewModel( selectedStudents = persons ) } - - } catch (e: Throwable) { - Napier.e("EDIT_GROUP: ERROR loading group", throwable = e) - } } } viewModelScope.launch { @@ -191,7 +187,6 @@ class StudentGroupingEditViewModel( } viewModelScope.launch { - try { val members = _uiState.value.selectedStudents.map { person -> XapiAgent( name = person.fullName(), @@ -261,9 +256,6 @@ class StudentGroupingEditViewModel( sendResultAndPop(RESULT_KEY_GROUP_UPDATED, true) } - } catch (e: Throwable) { - Napier.e("onClickSave ERROR", throwable = e) - } } } diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index 6fd1e8313..dd3d1d7de 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -7,6 +7,7 @@ const val VERB_COMPLETED = "http://adlnet.gov/expapi/verbs/completed" const val VERB_PROGRESSED = "http://adlnet.gov/expapi/verbs/progressed" const val VERB_SAVED = "http://adlnet.gov/expapi/verbs/saved" + const val VERB_VOIDED = "http://adlnet.gov/expapi/verbs/voided" @Serializable From 30b418c762d16bffc7a8c22afe51e2b582a2201c Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 11:38:51 +0530 Subject: [PATCH 039/130] Add `statementId` to student grouping navigation routes and update associated ViewModels to track and pass xAPI statement identifiers. Specific changes: * Add optional `statementId` parameter to the `StudentGroupingDetail` route. * Update `ClazzDetailViewModel` to extract `statementId` from xAPI statements and pass it when navigating to group details. * Update `StudentGroupingEditViewModel` to store the `statementId` from existing groups and include it in navigation commands. * Include `statementId` in `GroupDisplayData` and UI states for better tracking of group-related xAPI statements. * Add debug logging for group and statement identifiers across Clazz Detail, Student Grouping Detail, and Edit screens. --- .../respect/shared/navigation/AppRoutes.kt | 3 +- .../clazz/detail/ClazzDetailViewModel.kt | 43 ++++++++++++++++--- .../detail/StudentGroupingDetailViewModel.kt | 19 +++++++- .../edit/StudentGroupingEditViewModel.kt | 40 +++++++++++++---- 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 118e4ab62..4b85c6854 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -228,7 +228,8 @@ data class StudentGroupingEdit( @Serializable data class StudentGroupingDetail( val groupId: String, - val classId: String + val classId: String, + val statementId: String? = null ): RespectAppRoute @Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index f43e74af0..6f369a3dc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -93,6 +93,8 @@ data class ClazzDetailUiState( val isStudentGroupingExpanded: Boolean = true, val groupIds: List = emptyList(), val groups: List = emptyList(), + + val statementId: String? = null ) data class GroupDisplayData( @@ -100,6 +102,7 @@ data class GroupDisplayData( val groupName: String, val memberCount: Int, val memberNames: List = emptyList(), + val statementId: String? = null ) class ClazzDetailViewModel( @@ -332,11 +335,20 @@ class ClazzDetailViewModel( } fun onClickGroup(groupId: String) { + val groupData = _uiState.value.groups.find { it.groupId == groupId } + val statementId = groupData?.statementId + + Napier.d("=== NAVIGATE TO GROUP DETAIL ===") + Napier.d("Group ID: $groupId") + Napier.d("Statement ID: $statementId") + Napier.d("================================") + _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingDetail( groupId = groupId, - classId = route.guid + classId = route.guid, + statementId = statementId ) ) ) @@ -426,18 +438,37 @@ class ClazzDetailViewModel( val statementResult = dataLoadState.dataOrNull() ?: return@collect - val groups = statementResult.statements.mapNotNull { statement -> - statement.`object` as? XapiGroup - }.distinctBy { it.account?.name } + // Create a map of groupId to statement for quick lookup + val groupIdToStatement = statementResult.statements + .mapNotNull { statement -> + val group = statement.`object` as? XapiGroup + val groupId = group?.account?.name + if (groupId != null && group != null) { + groupId to statement + } else { + null + } + } + .distinctBy { it.first } + .toMap() - val groupDisplayDataList = groups.map { group -> + val groupDisplayDataList = groupIdToStatement.map { (groupId, statement) -> + val group = statement.`object` as XapiGroup val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + val statementId = statement.id?.toString() + + Napier.d("=== CLAZZ DETAIL GROUP DEBUG ===") + Napier.d("Group ID: $groupId") + Napier.d("Group Name: ${group.name}") + Napier.d("Statement ID: $statementId") + Napier.d("================================") GroupDisplayData( - groupId = group.account?.name ?: "", + groupId = groupId, groupName = group.name ?: "", memberCount = memberNames.size, memberNames = memberNames, + statementId = statementId ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 2e9a9c46f..317f7863c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -159,6 +159,14 @@ class StudentGroupingDetailViewModel( private fun loadGroupDetail() { viewModelScope.launch { try { + // If statement ID is provided in route, we can use it directly + val statementIdFromRoute = route.statementId + + Napier.d("=== STUDENT GROUPING DETAIL LOAD ===") + Napier.d("Group ID from route: ${route.groupId}") + Napier.d("Statement ID from route: $statementIdFromRoute") + Napier.d("====================================") + schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, @@ -178,12 +186,21 @@ class StudentGroupingDetailViewModel( if (groupStatement != null) { val group = groupStatement.`object` as XapiGroup val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + val statementId = groupStatement.id?.toString() + + Napier.d("=== STUDENT GROUPING DETAIL LOADED ===") + Napier.d("Group ID: ${group.account?.name}") + Napier.d("Group Name: ${group.name}") + Napier.d("Statement ID from query: $statementId") + Napier.d("Statement ID from route: $statementIdFromRoute") + Napier.d("Match: ${statementId == statementIdFromRoute}") + Napier.d("======================================") _uiState.update { prev -> prev.copy( groupName = group.name ?: "", groupMembers = memberNames, - statementGroupId = groupStatement.id?.toString() + statementGroupId = statementId ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index a0574ca94..6844ce67e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -60,7 +60,8 @@ data class StudentGroupingEditUiState( val groupName: String = "", val groupNameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val selectedStudents: List = emptyList() + val selectedStudents: List = emptyList(), + val statementId: String? = null ) { val personName: String get() = selectedAccount?.person?.fullName() ?: "" @@ -128,8 +129,7 @@ class StudentGroupingEditViewModel( route.groupId?.let { groupId -> @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { - - val statementResult = schoolDataSource.xapiStatementsResource.get( + val statementResult = schoolDataSource.xapiStatementsResource.get( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, activity = classActivityId, @@ -138,11 +138,25 @@ class StudentGroupingEditViewModel( dataLoadParams = DataLoadParams() ).dataOrNull() ?: return@launch - val group = statementResult.statements - .mapNotNull { statement -> statement.`object` as? XapiGroup } - .firstOrNull { it.account?.name == groupId } + val groupStatement = statementResult.statements + .firstOrNull { statement -> + val group = statement.`object` as? XapiGroup + group?.account?.name == groupId + } ?: return@launch + val group = groupStatement.`object` as XapiGroup + + val groupId = group.account?.name + val groupName = group.name + val statementId = groupStatement.id?.toString() + + Napier.d("=== STUDENT GROUPING EDIT DEBUG ===") + Napier.d("Group ID: $groupId") + Napier.d("Group Name: $groupName") + Napier.d("Statement ID: $statementId") + Napier.d("===================================") + val memberIds = group.member ?.mapNotNull { it.account?.name } ?: emptyList() @@ -156,8 +170,9 @@ class StudentGroupingEditViewModel( _uiState.update { prev -> prev.copy( - groupName = group.name ?: "", - selectedStudents = persons + groupName = groupName ?: "", + selectedStudents = persons, + statementId = statementId ) } } @@ -176,6 +191,13 @@ class StudentGroupingEditViewModel( val groupName = _uiState.value.groupName val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" + val existingStatementId = _uiState.value.statementId + + Napier.d("=== STUDENT SAVE GROUP DEBUG ===") + Napier.d("Group ID from route: ${route.groupId}") + Napier.d("Group Name: $groupName") + Napier.d("Existing Statement ID: $existingStatementId") + Napier.d("========================") if (groupName.isBlank()) { _uiState.update { @@ -247,7 +269,7 @@ class StudentGroupingEditViewModel( if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingDetail(groupId, route.classUid), + StudentGroupingDetail(groupId, route.classUid, existingStatementId), popUpTo = route, popUpToInclusive = true ) From fe27bd6632e551978700e8d6748fe0b20d922778 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 12:48:14 +0530 Subject: [PATCH 040/130] Implement xAPI statement voiding when updating a student group in `StudentGroupingEditViewModel`. When editing an existing group, a voiding statement is sent for the old statement ID before posting the updated group information as a new statement. --- .../edit/StudentGroupingEditViewModel.kt | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 6844ce67e..214ae4cdf 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -22,6 +22,7 @@ import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiAgent @@ -32,6 +33,7 @@ import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiObjectType import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiStatementRef import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.libutil.util.time.localDateInCurrentTimeZone @@ -209,6 +211,43 @@ class StudentGroupingEditViewModel( } viewModelScope.launch { + // Step 1: If editing an existing group, void the old statement first + if (existingStatementId != null) { + Napier.d("=== VOIDING EXISTING STATEMENT ===") + Napier.d("Statement ID to void: $existingStatementId") + + val actor = XapiAgent( + name = _uiState.value.personName, + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = _uiState.value.personId, + homePage = schoolSelfUrl.toString() + ) + ) + + val voidVerb = XapiVerb( + id = VERB_VOIDED, + display = mapOf("en-US" to "voided") + ) + + val statementRef = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = existingStatementId + ) + + val voidingStatement = XapiStatement( + actor = actor, + verb = voidVerb, + `object` = statementRef, + timestamp = Clock.System.now() + ) + + schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + + Napier.d("=== VOIDING COMPLETE ===") + } + + // Step 2: Create a new statement with the updated group information val members = _uiState.value.selectedStudents.map { person -> XapiAgent( name = person.fullName(), @@ -220,6 +259,7 @@ class StudentGroupingEditViewModel( ) } + // Always use the same groupId, but create a new statement ID val groupId = route.groupId ?: Uuid.random().toString() val group = XapiGroup( @@ -266,10 +306,14 @@ class StudentGroupingEditViewModel( schoolDataSource.xapiStatementsResource.post(listOf(statement)) + Napier.d("=== NEW STATEMENT SAVED ===") + Napier.d("Group ID: $groupId") + Napier.d("New Statement ID: ${statement.id}") + if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingDetail(groupId, route.classUid, existingStatementId), + StudentGroupingDetail(groupId, route.classUid, statement.id?.toString()), popUpTo = route, popUpToInclusive = true ) From 30117f6fdb41952aeee25aa4c969587ff77923b3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 12:50:38 +0530 Subject: [PATCH 041/130] Remove debug logging from `ClazzDetailViewModel` and `StudentGroupingEditViewModel`. --- .../clazz/detail/ClazzDetailViewModel.kt | 11 ---------- .../edit/StudentGroupingEditViewModel.kt | 21 ------------------- 2 files changed, 32 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 6f369a3dc..63399900b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -338,11 +338,6 @@ class ClazzDetailViewModel( val groupData = _uiState.value.groups.find { it.groupId == groupId } val statementId = groupData?.statementId - Napier.d("=== NAVIGATE TO GROUP DETAIL ===") - Napier.d("Group ID: $groupId") - Napier.d("Statement ID: $statementId") - Napier.d("================================") - _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingDetail( @@ -457,12 +452,6 @@ class ClazzDetailViewModel( val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() val statementId = statement.id?.toString() - Napier.d("=== CLAZZ DETAIL GROUP DEBUG ===") - Napier.d("Group ID: $groupId") - Napier.d("Group Name: ${group.name}") - Napier.d("Statement ID: $statementId") - Napier.d("================================") - GroupDisplayData( groupId = groupId, groupName = group.name ?: "", diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 214ae4cdf..8c4aa4910 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -3,7 +3,6 @@ package world.respect.shared.viewmodel.studentgrouping.edit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -153,12 +152,6 @@ class StudentGroupingEditViewModel( val groupName = group.name val statementId = groupStatement.id?.toString() - Napier.d("=== STUDENT GROUPING EDIT DEBUG ===") - Napier.d("Group ID: $groupId") - Napier.d("Group Name: $groupName") - Napier.d("Statement ID: $statementId") - Napier.d("===================================") - val memberIds = group.member ?.mapNotNull { it.account?.name } ?: emptyList() @@ -195,12 +188,6 @@ class StudentGroupingEditViewModel( val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" val existingStatementId = _uiState.value.statementId - Napier.d("=== STUDENT SAVE GROUP DEBUG ===") - Napier.d("Group ID from route: ${route.groupId}") - Napier.d("Group Name: $groupName") - Napier.d("Existing Statement ID: $existingStatementId") - Napier.d("========================") - if (groupName.isBlank()) { _uiState.update { it.copy(groupNameError = Res.string.required_field.asUiText()) @@ -213,8 +200,6 @@ class StudentGroupingEditViewModel( viewModelScope.launch { // Step 1: If editing an existing group, void the old statement first if (existingStatementId != null) { - Napier.d("=== VOIDING EXISTING STATEMENT ===") - Napier.d("Statement ID to void: $existingStatementId") val actor = XapiAgent( name = _uiState.value.personName, @@ -243,8 +228,6 @@ class StudentGroupingEditViewModel( ) schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) - - Napier.d("=== VOIDING COMPLETE ===") } // Step 2: Create a new statement with the updated group information @@ -306,10 +289,6 @@ class StudentGroupingEditViewModel( schoolDataSource.xapiStatementsResource.post(listOf(statement)) - Napier.d("=== NEW STATEMENT SAVED ===") - Napier.d("Group ID: $groupId") - Napier.d("New Statement ID: ${statement.id}") - if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( From 11a551bd4e8d3d174fb8ad0c741a17645458553e Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 13:13:53 +0530 Subject: [PATCH 042/130] Update `ClazzDetailViewModel`, `StudentGroupingDetailViewModel`, and `StudentGroupingEditViewModel` to filter out voided xAPI statements when retrieving groups. --- .../clazz/detail/ClazzDetailViewModel.kt | 19 ++++++++-- .../detail/StudentGroupingDetailViewModel.kt | 36 ++++++++----------- .../edit/StudentGroupingEditViewModel.kt | 17 +++++++-- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 63399900b..51e7f5e95 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -64,6 +64,7 @@ import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS @@ -201,7 +202,6 @@ class ClazzDetailViewModel( } } - // Observe groups from xAPI statements as a flow viewModelScope.launch { observeGroupsFromXapi() } @@ -433,12 +433,25 @@ class ClazzDetailViewModel( val statementResult = dataLoadState.dataOrNull() ?: return@collect - // Create a map of groupId to statement for quick lookup + // Get all voiding statements to filter out voided statements + val voidingStatements = statementResult.statements.filter { statement -> + statement.verb.id == VERB_VOIDED + } + + val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> + (voidingStmt.`object` as? world.respect.lib.xapi.model.XapiStatementRef)?.id + }.toSet() + val groupIdToStatement = statementResult.statements + .filter { statement -> + // Filter out: voiding statements and voided statements + statement.verb?.id != VERB_VOIDED && + statement.id?.toString() !in voidedStatementIds + } .mapNotNull { statement -> val group = statement.`object` as? XapiGroup val groupId = group?.account?.name - if (groupId != null && group != null) { + if (groupId != null) { groupId to statement } else { null diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 317f7863c..41d9e56b7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -158,15 +158,6 @@ class StudentGroupingDetailViewModel( @OptIn(ExperimentalUuidApi::class) private fun loadGroupDetail() { viewModelScope.launch { - try { - // If statement ID is provided in route, we can use it directly - val statementIdFromRoute = route.statementId - - Napier.d("=== STUDENT GROUPING DETAIL LOAD ===") - Napier.d("Group ID from route: ${route.groupId}") - Napier.d("Statement ID from route: $statementIdFromRoute") - Napier.d("====================================") - schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, @@ -177,7 +168,22 @@ class StudentGroupingDetailViewModel( ).collect { dataLoadState -> val statementResult = dataLoadState.dataOrNull() ?: return@collect + // Get all voiding statements to filter out voided statements + val voidingStatements = statementResult.statements.filter { statement -> + statement.verb?.id == VERB_VOIDED + } + + val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> + (voidingStmt.`object` as? XapiStatementRef)?.id + }.toSet() + + // Find the latest non-voided statement for this group val groupStatement = statementResult.statements + .filter { statement -> + // Filter out voiding statements and voided statements + statement.verb.id != VERB_VOIDED && + statement.id?.toString() !in voidedStatementIds + } .firstOrNull { statement -> val group = statement.`object` as? XapiGroup group?.account?.name == route.groupId @@ -187,15 +193,6 @@ class StudentGroupingDetailViewModel( val group = groupStatement.`object` as XapiGroup val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() val statementId = groupStatement.id?.toString() - - Napier.d("=== STUDENT GROUPING DETAIL LOADED ===") - Napier.d("Group ID: ${group.account?.name}") - Napier.d("Group Name: ${group.name}") - Napier.d("Statement ID from query: $statementId") - Napier.d("Statement ID from route: $statementIdFromRoute") - Napier.d("Match: ${statementId == statementIdFromRoute}") - Napier.d("======================================") - _uiState.update { prev -> prev.copy( groupName = group.name ?: "", @@ -211,9 +208,6 @@ class StudentGroupingDetailViewModel( } } } - } catch (e: Throwable) { - Napier.e("loadGroupDetail ERROR", throwable = e) - } } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 8c4aa4910..f4836796b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -139,7 +139,22 @@ class StudentGroupingEditViewModel( dataLoadParams = DataLoadParams() ).dataOrNull() ?: return@launch + // Get all voiding statements to filter out voided statements + val voidingStatements = statementResult.statements.filter { statement -> + statement.verb?.id == VERB_VOIDED + } + + val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> + (voidingStmt.`object` as? XapiStatementRef)?.id + }.toSet() + + // Find the latest non-voided statement for this group val groupStatement = statementResult.statements + .filter { statement -> + // Filter out voiding statements and voided statements + statement.verb?.id != VERB_VOIDED && + statement.id?.toString() !in voidedStatementIds + } .firstOrNull { statement -> val group = statement.`object` as? XapiGroup group?.account?.name == groupId @@ -148,7 +163,6 @@ class StudentGroupingEditViewModel( val group = groupStatement.`object` as XapiGroup - val groupId = group.account?.name val groupName = group.name val statementId = groupStatement.id?.toString() @@ -242,7 +256,6 @@ class StudentGroupingEditViewModel( ) } - // Always use the same groupId, but create a new statement ID val groupId = route.groupId ?: Uuid.random().toString() val group = XapiGroup( From e2640a32498a18e7b559270b070601fbe44e8d98 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 13:15:09 +0530 Subject: [PATCH 043/130] Update `ClazzDetailViewModel`, `StudentGroupingDetailViewModel`, and `StudentGroupingEditViewModel` to filter out voided xAPI statements when retrieving groups. --- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 2 +- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 51e7f5e95..042db0ccd 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -445,7 +445,7 @@ class ClazzDetailViewModel( val groupIdToStatement = statementResult.statements .filter { statement -> // Filter out: voiding statements and voided statements - statement.verb?.id != VERB_VOIDED && + statement.verb.id != VERB_VOIDED && statement.id?.toString() !in voidedStatementIds } .mapNotNull { statement -> diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index f4836796b..1640c9168 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -141,7 +141,7 @@ class StudentGroupingEditViewModel( // Get all voiding statements to filter out voided statements val voidingStatements = statementResult.statements.filter { statement -> - statement.verb?.id == VERB_VOIDED + statement.verb.id == VERB_VOIDED } val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> @@ -152,7 +152,7 @@ class StudentGroupingEditViewModel( val groupStatement = statementResult.statements .filter { statement -> // Filter out voiding statements and voided statements - statement.verb?.id != VERB_VOIDED && + statement.verb.id != VERB_VOIDED && statement.id?.toString() !in voidedStatementIds } .firstOrNull { statement -> From 3bc78c66189998462c4828aec18cb72266b2406c Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 15:06:13 +0530 Subject: [PATCH 044/130] Simplify xAPI statement retrieval by using timestamps to identify the latest group state and refactor `StudentGroupingDetailViewModel` to inject `NavResultReturner` via the constructor. --- .../clazz/detail/ClazzDetailViewModel.kt | 19 +--- .../detail/StudentGroupingDetailViewModel.kt | 88 ++++++++----------- .../edit/StudentGroupingEditViewModel.kt | 29 +++--- 3 files changed, 51 insertions(+), 85 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 042db0ccd..3b10c00f9 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -64,7 +64,6 @@ import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED -import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS @@ -433,21 +432,11 @@ class ClazzDetailViewModel( val statementResult = dataLoadState.dataOrNull() ?: return@collect - // Get all voiding statements to filter out voided statements - val voidingStatements = statementResult.statements.filter { statement -> - statement.verb.id == VERB_VOIDED - } - - val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> - (voidingStmt.`object` as? world.respect.lib.xapi.model.XapiStatementRef)?.id - }.toSet() - + // Sort by timestamp descending (latest first) and take first per groupId + // This ensures we get the latest version of each group val groupIdToStatement = statementResult.statements - .filter { statement -> - // Filter out: voiding statements and voided statements - statement.verb.id != VERB_VOIDED && - statement.id?.toString() !in voidedStatementIds - } + .filter { it.verb.id == VERB_SAVED } + .sortedByDescending { it.timestamp ?: it.stored } .mapNotNull { statement -> val group = statement.`object` as? XapiGroup val groupId = group?.account?.name diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 41d9e56b7..ef54d5ae9 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -38,6 +38,7 @@ import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue import kotlin.time.Clock +import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi @@ -51,18 +52,14 @@ data class StudentGroupingDetailUiState( class StudentGroupingDetailViewModel( savedStateHandle: SavedStateHandle, private val respectAccountManager: RespectAccountManager, + private val navResultReturner: NavResultReturner ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = respectAccountManager.requireActiveAccountScope() - private val _uiState = MutableStateFlow(StudentGroupingDetailUiState()) - val uiState = _uiState.asStateFlow() - private val schoolDataSource: SchoolDataSource by inject() - private val route: StudentGroupingDetail = savedStateHandle.toRoute() - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self?.toString() val classActivityId = "${schoolSelfUrl}${CLASS}${route.classId}" @@ -77,9 +74,9 @@ class StudentGroupingDetailViewModel( ) ) } + loadGroupDetail() - val navResultReturner: NavResultReturner = getKoin().get() viewModelScope.launch { navResultReturner.filteredResultFlowForKey( RESULT_KEY_GROUP_UPDATED @@ -158,56 +155,43 @@ class StudentGroupingDetailViewModel( @OptIn(ExperimentalUuidApi::class) private fun loadGroupDetail() { viewModelScope.launch { - schoolDataSource.xapiStatementsResource.getAsFlow( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_SAVED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).collect { dataLoadState -> - val statementResult = dataLoadState.dataOrNull() ?: return@collect - - // Get all voiding statements to filter out voided statements - val voidingStatements = statementResult.statements.filter { statement -> - statement.verb?.id == VERB_VOIDED + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).collect { dataLoadState -> + val statementResult = dataLoadState.dataOrNull() ?: return@collect + + // Find the latest statement for this group by sorting by timestamp + val groupStatement = statementResult.statements + .filter { statement -> + val group = statement.`object` as? XapiGroup + group?.account?.name == route.groupId + } + .maxByOrNull { it.timestamp ?: it.stored ?: Instant.DISTANT_PAST } + + if (groupStatement != null) { + val group = groupStatement.`object` as XapiGroup + val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + val statementId = groupStatement.id?.toString() + _uiState.update { prev -> + prev.copy( + groupName = group.name ?: "", + groupMembers = memberNames, + statementGroupId = statementId + ) } - val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> - (voidingStmt.`object` as? XapiStatementRef)?.id - }.toSet() - - // Find the latest non-voided statement for this group - val groupStatement = statementResult.statements - .filter { statement -> - // Filter out voiding statements and voided statements - statement.verb.id != VERB_VOIDED && - statement.id?.toString() !in voidedStatementIds - } - .firstOrNull { statement -> - val group = statement.`object` as? XapiGroup - group?.account?.name == route.groupId - } - - if (groupStatement != null) { - val group = groupStatement.`object` as XapiGroup - val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - val statementId = groupStatement.id?.toString() - _uiState.update { prev -> - prev.copy( - groupName = group.name ?: "", - groupMembers = memberNames, - statementGroupId = statementId - ) - } - - _appUiState.update { - it.copy( - title = group.name?.asUiText() ?: "".asUiText() - ) - } + _appUiState.update { + it.copy( + title = group.name?.asUiText() ?: "".asUiText() + ) } } + } } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 1640c9168..c222173ca 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -52,6 +52,7 @@ import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import kotlin.getValue import kotlin.time.Clock +import kotlin.time.Instant import kotlin.toString import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -79,16 +80,18 @@ class StudentGroupingEditViewModel( respectAccountManager: RespectAccountManager ) : RespectViewModel(savedStateHandle), KoinScopeComponent { - override val scope: Scope = respectAccountManager.requireActiveAccountScope() private val _uiState = MutableStateFlow(StudentGroupingEditUiState()) + val uiState = _uiState.asStateFlow() private val schoolDataSource: SchoolDataSource by inject() + private val route: StudentGroupingEdit = savedStateHandle.toRoute() val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" private fun pagingSourceByRole(): PagingSourceFactoryHolder { @@ -127,6 +130,7 @@ class StudentGroupingEditViewModel( _uiState.update { it.copy(students = studentPagingSource) } + route.groupId?.let { groupId -> @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { @@ -139,26 +143,15 @@ class StudentGroupingEditViewModel( dataLoadParams = DataLoadParams() ).dataOrNull() ?: return@launch - // Get all voiding statements to filter out voided statements - val voidingStatements = statementResult.statements.filter { statement -> - statement.verb.id == VERB_VOIDED - } - - val voidedStatementIds = voidingStatements.mapNotNull { voidingStmt -> - (voidingStmt.`object` as? XapiStatementRef)?.id - }.toSet() - - // Find the latest non-voided statement for this group + // Find the latest statement for this group by sorting by timestamp val groupStatement = statementResult.statements .filter { statement -> - // Filter out voiding statements and voided statements - statement.verb.id != VERB_VOIDED && - statement.id?.toString() !in voidedStatementIds - } - .firstOrNull { statement -> val group = statement.`object` as? XapiGroup group?.account?.name == groupId } + .maxByOrNull { + it.timestamp ?: it.stored ?: Instant.DISTANT_PAST + } ?: return@launch val group = groupStatement.`object` as XapiGroup @@ -212,7 +205,7 @@ class StudentGroupingEditViewModel( } viewModelScope.launch { - // Step 1: If editing an existing group, void the old statement first + // If editing an existing group, void the old statement first if (existingStatementId != null) { val actor = XapiAgent( @@ -244,7 +237,7 @@ class StudentGroupingEditViewModel( schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) } - // Step 2: Create a new statement with the updated group information + // Create a new statement with the updated group information val members = _uiState.value.selectedStudents.map { person -> XapiAgent( name = person.fullName(), From 55392bf9c408c8d50c9b1b326e58defaaf48fbb0 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 15:15:53 +0530 Subject: [PATCH 045/130] Refactor `ClazzDetailViewModel.kt` to improve code formatting and update enrollment removal logic to mark records as `TO_BE_DELETED` if they were created on the current day. --- .../clazz/detail/ClazzDetailViewModel.kt | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 3b10c00f9..202f551f3 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -69,10 +69,10 @@ import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS data class ClazzDetailUiState( - val teachers: IPagingSourceFactory = EmptyPagingSourceFactory(), + val teachers: IPagingSourceFactory = EmptyPagingSourceFactory() , val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val pendingTeachers: IPagingSourceFactory = EmptyPagingSourceFactory(), - val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory(), + val pendingTeachers:IPagingSourceFactory = EmptyPagingSourceFactory() , + val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory() , val listOfPending: List = emptyList(), val chipOptions: List = emptyList(), @@ -142,9 +142,9 @@ class ClazzDetailViewModel( } } - private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) + private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) - private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) private val teachersPendingPagingSource = pagingSourceByRole(EnrollmentRoleEnum.PENDING_TEACHER) @@ -194,10 +194,7 @@ class ClazzDetailViewModel( _appUiState.update { it.copy(title = clazz.dataOrNull()?.title?.asUiText()) } - - _uiState.update { - it.copy(clazz = clazz) - } + _uiState.update { it.copy(clazz = clazz) } } } @@ -252,7 +249,7 @@ class ClazzDetailViewModel( ) ) ) - } catch (e: Throwable) { + }catch(e: Throwable) { e.printStackTrace() } } @@ -282,7 +279,7 @@ class ClazzDetailViewModel( ) ) ) - } + } } fun onSortOrderChanged(sortOption: SortOrderOption) { @@ -302,7 +299,7 @@ class ClazzDetailViewModel( personUid = user.guid, approved = true, ) - } catch (e: Throwable) { + }catch(e: Throwable) { e.printStackTrace() } } @@ -370,9 +367,9 @@ class ClazzDetailViewModel( }.map { it.copy( lastModified = modTime, - status = if (it.beginDate == today) { - StatusEnum.TO_BE_DELETED - } else { + status = if(it.beginDate == today) { + StatusEnum.TO_BE_DELETED //probably was just added by mistake + }else { it.status }, endDate = today, @@ -382,7 +379,8 @@ class ClazzDetailViewModel( schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) - } catch (e: Throwable) { + }catch(e: Throwable) { + //do something Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) } From 7089f572a2e4338cbf85a73e3f68ca418ffef78b Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 15:31:23 +0530 Subject: [PATCH 046/130] Clean up whitespace in `StudentGroupingDetailViewModel.kt`. --- .../studentgrouping/detail/StudentGroupingDetailViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index ef54d5ae9..8b6f3de36 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -145,8 +145,8 @@ class StudentGroupingDetailViewModel( stored = Clock.System.now() ) - schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) _navCommandFlow.tryEmit(NavCommand.PopUp()) } From 5288f96e0bf0b655131f9b961f95396d0e7adad7 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 16:19:13 +0530 Subject: [PATCH 047/130] Implement xAPI group deletion and refine the student grouping data model. - Implement `onConfirmDeleteGroup` in `StudentGroupingDetailViewModel` using xAPI `VOIDED` statements. - Update `StudentGroupingDetailUiState` to include the selected account and helper properties for person name and ID. - Remove unused `metadata` field and related `GROUP_IDS` constant from the `Clazz` model and `ClassEntity` database schema. - Fix a bug in `XapiStatementEntityDao` and `XapiStatementEntityJsonDao` where `statementVerbId` was incorrectly referenced as `statementVerbUid`. - Clean up UI code in `StudentGroupingEditScreen` and `StudentGroupingDetailScreen`, including improved paging keys and formatting. - Remove the unused `findGroupByAccountNameAsync` from `XapiActorDao`. - Define `SAVED` and `VOIDED` display constants in `XapiVerb`. --- .../UstadExposedDropDownMenuField.kt | 2 -- .../detail/StudentGroupingDetailScreen.kt | 4 +-- .../edit/StudentGroupingEditScreen.kt | 14 ++------ .../db/school/adapters/ClassAdapter.kt | 2 -- .../db/school/entities/ClassEntity.kt | 2 -- .../db/school/xapi/daos/XapiActorDao.kt | 10 +----- .../xapi/daos/XapiStatementEntityDao.kt | 2 +- .../xapi/daos/XapiStatementEntityJsonDao.kt | 2 +- .../respect/datalayer/school/model/Clazz.kt | 4 --- .../clazz/detail/ClazzDetailViewModel.kt | 9 ++--- .../detail/StudentGroupingDetailViewModel.kt | 36 +++++++++++++------ .../edit/StudentGroupingEditViewModel.kt | 10 +++--- .../world/respect/lib/xapi/model/XapiVerb.kt | 5 +++ 13 files changed, 47 insertions(+), 55 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt index face82962..1fc933f80 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/components/UstadExposedDropDownMenuField.kt @@ -34,7 +34,6 @@ fun RespectExposedDropDownMenuField( var expanded by remember { mutableStateOf(false) } - ExposedDropdownMenuBox( modifier = modifier, expanded = expanded, @@ -75,7 +74,6 @@ fun RespectExposedDropDownMenuField( ) } } - } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 7cc4e5d86..98e470c01 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -123,7 +123,7 @@ fun StudentGroupingDetailScreen( } } - item("divider2 ") { + item("divider2") { HorizontalDivider() } @@ -135,7 +135,7 @@ fun StudentGroupingDetailScreen( }, headlineContent = { Text(text = uiState.groupMembers[index]) - }, + } ) } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 6b016f3ac..710cce4a0 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -47,23 +47,15 @@ fun StudentGroupingEditScreen( fun StudentGroupingEditScreen( uiState: StudentGroupingEditUiState, onStudentCheckedChange: (Person, Boolean) -> Unit, - onGroupNameChanged: (String) -> Unit, - - ) { + onGroupNameChanged: (String) -> Unit +) { val studentPager = respectRememberPager(uiState.students) val studentLazyPagingItems = studentPager.flow.collectAsLazyPagingItems() - fun Person?.key(role: EnrollmentRoleEnum, index: Int): Any { - return this?.guid?.let { - Pair(it, role) - } ?: "${role}_$index" - } - LazyColumn( modifier = Modifier.fillMaxSize() ) { - item { OutlinedTextField( modifier = Modifier.fillMaxWidth() @@ -91,7 +83,7 @@ fun StudentGroupingEditScreen( respectPagingItems( items = studentLazyPagingItems, key = { person, index -> - person?.guid ?: "student_$index" + person?.guid ?: index } ) { student -> diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt index 1c9701b3b..cfb7d9dcc 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/adapters/ClassAdapter.kt @@ -22,7 +22,6 @@ fun ClassEntities.toModel(): Clazz { guid = clazz.cGuid, title = clazz.cTitle, status = clazz.cStatus, - metadata = clazz.cMetadata, description = clazz.cDescription, lastModified = clazz.cLastModified, stored = clazz.cStored, @@ -55,7 +54,6 @@ fun Clazz.toEntities( cGuidHash = classUidNum, cTitle = title, cStatus = status, - cMetadata=metadata, cDescription = description, cLastModified = lastModified, cStored = stored, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt index beac03d48..891a4efbf 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/entities/ClassEntity.kt @@ -2,7 +2,6 @@ package world.respect.datalayer.db.school.entities import androidx.room.Entity import androidx.room.PrimaryKey -import kotlinx.serialization.json.JsonObject import world.respect.datalayer.school.model.StatusEnum import kotlin.time.Instant @@ -13,7 +12,6 @@ data class ClassEntity( val cGuidHash: Long, val cTitle: String, val cStatus: StatusEnum, - val cMetadata: JsonObject?, val cDescription: String?, val cLastModified: Instant, val cStored: Instant, diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt index a861decaf..49fbe6ea3 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiActorDao.kt @@ -80,13 +80,5 @@ interface XapiActorDao { uids: List ): List - @Query( - """ - SELECT XapiActorEntity.* - FROM XapiActorEntity - WHERE XapiActorEntity.actorAccountName = :accountName - LIMIT 1 - """ - ) - suspend fun findGroupByAccountNameAsync(accountName: String): XapiActorEntity? + } \ No newline at end of file diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt index defdaf789..aaeae5b10 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt @@ -156,7 +156,7 @@ interface XapiStatementEntityDao { ) ) ) - AND (:verbUid = 0 OR XapiStatementEntity.statementVerbUid = :verbUid) + AND (:verbUid = 0 OR XapiStatementEntity.statementVerbId = :verbUid) -- Handle activity parameter AND ( :activityUid = 0 OR ( XapiStatementEntity.statementObjectType = ${XapiEntityObjectTypeFlags.ACTIVITY} diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt index c0377ce5d..6f4214f36 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt @@ -129,7 +129,7 @@ interface XapiStatementEntityJsonDao { ) ) ) - AND (:verbUid = 0 OR XapiStatementEntity.statementVerbUid = :verbUid) + AND (:verbUid = 0 OR XapiStatementEntity.statementVerbId = :verbUid) -- Handle activity parameter AND ( :activityUid = 0 OR ( XapiStatementEntity.statementObjectType = ${XapiEntityObjectTypeFlags.ACTIVITY} diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt index 8e6e8b972..af5be7a92 100644 --- a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt +++ b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/model/Clazz.kt @@ -1,7 +1,6 @@ package world.respect.datalayer.school.model import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject import world.respect.datalayer.shared.ModelWithTimes import world.respect.lib.serializers.InstantAsISO8601 import kotlin.random.Random @@ -13,7 +12,6 @@ data class Clazz( val title: String, val status: StatusEnum = StatusEnum.ACTIVE, val description: String? = null, - val metadata: JsonObject? = null, override val lastModified: InstantAsISO8601 = Clock.System.now(), override val stored: InstantAsISO8601 = Clock.System.now(), val teacherInviteGuid: String? = null, @@ -38,8 +36,6 @@ data class Clazz( const val DEFAULT_INVITE_CODE_MAX = 100_000 const val DEFAULT_INVITE_CODE_LEN = 6 - - const val GROUP_IDS = "groupIds" } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 202f551f3..20a75a8d8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -93,7 +93,6 @@ data class ClazzDetailUiState( val isStudentGroupingExpanded: Boolean = true, val groupIds: List = emptyList(), val groups: List = emptyList(), - val statementId: String? = null ) @@ -127,6 +126,7 @@ class ClazzDetailViewModel( private val route: ClazzDetail = savedStateHandle.toRoute() val schoolSelfUrl = accountManager.activeAccount?.school?.self?.toString() + val classActivityId = "${schoolSelfUrl}${CLASS}${route.guid}" private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { @@ -209,9 +209,8 @@ class ClazzDetailViewModel( prev.copy( showAddStudent = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, showAddTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - showStudentGrouping = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - - ) + showStudentGrouping = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true + ) } _appUiState.update { @@ -345,7 +344,6 @@ class ClazzDetailViewModel( ) } - fun onClickRemovePersonFromClass(person: Person, role: EnrollmentRoleEnum) { viewModelScope.launch { try { @@ -467,7 +465,6 @@ class ClazzDetailViewModel( } } - companion object { const val ALL = "All" diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 8b6f3de36..e7bd02fdc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -12,10 +12,12 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.ext.dataOrNull import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.xapi.model.VOIDED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiGroup @@ -35,19 +37,26 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS +import world.respect.shared.domain.account.RespectSessionAndPerson import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi - data class StudentGroupingDetailUiState( + val selectedAccount: RespectSessionAndPerson? = null, val groupName: String = "", val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, val statementGroupId: String? = null, -) +) { + val personName: String + get() = selectedAccount?.person?.fullName() ?: "" + + val personId: String + get() = selectedAccount?.person?.guid ?: "" +} class StudentGroupingDetailViewModel( savedStateHandle: SavedStateHandle, @@ -56,11 +65,17 @@ class StudentGroupingDetailViewModel( ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = respectAccountManager.requireActiveAccountScope() + private val _uiState = MutableStateFlow(StudentGroupingDetailUiState()) + val uiState = _uiState.asStateFlow() + private val schoolDataSource: SchoolDataSource by inject() + private val route: StudentGroupingDetail = savedStateHandle.toRoute() + val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self?.toString() + val classActivityId = "${schoolSelfUrl}${CLASS}${route.classId}" init { @@ -110,8 +125,8 @@ class StudentGroupingDetailViewModel( _uiState.update { it.copy(showDeleteGroupDialog = false) } viewModelScope.launch { - val statementGroupId = _uiState.value.statementGroupId - if (statementGroupId == null) { + val statementId = _uiState.value.statementGroupId + if (statementId == null) { Napier.w("onConfirmDeleteGroup: Statement ID not found") return@launch } @@ -119,34 +134,33 @@ class StudentGroupingDetailViewModel( val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self val actor = XapiAgent( - name = respectAccountManager.activeAccount?.userGuid ?: "", + name = _uiState.value.personName, objectType = XapiObjectType.Agent, account = XapiAccount( - name = respectAccountManager.activeAccount?.userGuid ?: "", + name = _uiState.value.personId, homePage = schoolSelfUrl.toString() ) ) val verb = XapiVerb( id = VERB_VOIDED, - display = mapOf("en" to VERB_VOIDED) + display = mapOf("en-US" to VOIDED) ) val statementRef = XapiStatementRef( objectType = XapiObjectType.StatementRef, - id = statementGroupId + id = statementId ) val voidingStatement = XapiStatement( actor = actor, verb = verb, `object` = statementRef, - timestamp = Clock.System.now(), - stored = Clock.System.now() + timestamp = Clock.System.now() ) - schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + _navCommandFlow.tryEmit(NavCommand.PopUp()) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index c222173ca..35629c446 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -20,8 +20,10 @@ import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.lib.dataloadstate.DataLoadParams +import world.respect.lib.xapi.model.SAVED import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.xapi.model.VOIDED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiAgent @@ -219,7 +221,7 @@ class StudentGroupingEditViewModel( val voidVerb = XapiVerb( id = VERB_VOIDED, - display = mapOf("en-US" to "voided") + display = mapOf("en-US" to VOIDED) ) val statementRef = XapiStatementRef( @@ -270,10 +272,10 @@ class StudentGroupingEditViewModel( ) ) - val verbId = VERB_SAVED + val verb = XapiVerb( - id = verbId, - display = mapOf("en" to verbId) + id = VERB_SAVED, + display = mapOf("en-US" to SAVED) ) val statement = XapiStatement( diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index dd3d1d7de..889ecd232 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -6,10 +6,15 @@ import kotlinx.serialization.Serializable const val VERB_COMPLETED = "http://adlnet.gov/expapi/verbs/completed" const val VERB_PROGRESSED = "http://adlnet.gov/expapi/verbs/progressed" + const val VERB_SAVED = "http://adlnet.gov/expapi/verbs/saved" const val VERB_VOIDED = "http://adlnet.gov/expapi/verbs/voided" +const val SAVED = "saved" + +const val VOIDED = "voided" + @Serializable data class XapiVerb( val id: String, From 1c842b5af1e1e2f61121758243b8b6c49d791ef9 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 17:00:35 +0530 Subject: [PATCH 048/130] Relocate the `isIdentified` property within `XapiGroup.kt` and remove trailing whitespace. --- .../kotlin/world/respect/lib/xapi/model/XapiGroup.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt index 5691da1a5..6a3036ffd 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiGroup.kt @@ -28,14 +28,12 @@ data class XapiGroup( get() = name == null && mbox == null && mbox_sha1sum == null && openid == null && account == null + val isIdentified: Boolean = !isAnonymous + companion object { const val RESULT_KEY_GROUP_UPDATED = "groupUpdated" const val CLASS = "class/" } - - val isIdentified: Boolean = !isAnonymous - } - From c284dc329e847b6f2a932ca1bb931e97d49b15a8 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 17:55:02 +0530 Subject: [PATCH 049/130] Update `StudentGroupingDetailViewModel` and `StudentGroupingEditViewModel` to support direct xAPI statement lookups and explicit statement ID generation. - Update `StudentGroupingDetailViewModel` to query xAPI statements by `statementId` if available in the route, falling back to verb and activity filters. - Modify `StudentGroupingEditViewModel` to pre-generate a `Uuid` for new xAPI statements and pass it to the `StudentGroupingDetail` navigation route. --- .../detail/StudentGroupingDetailViewModel.kt | 20 ++++++++++++++++--- .../edit/StudentGroupingEditViewModel.kt | 6 +++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index e7bd02fdc..134912190 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -43,6 +43,7 @@ import kotlin.getValue import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid data class StudentGroupingDetailUiState( val selectedAccount: RespectSessionAndPerson? = null, @@ -169,12 +170,25 @@ class StudentGroupingDetailViewModel( @OptIn(ExperimentalUuidApi::class) private fun loadGroupDetail() { viewModelScope.launch { - schoolDataSource.xapiStatementsResource.getAsFlow( - listParams = XapiStatementsResource.GetStatementParams( + // If we have a statementId, query by it directly, otherwise query by verb and activity + val statementIdUuid = route.statementId?.let { Uuid.parse(it) } + + val listParams = if (statementIdUuid != null) { + // When we have a specific statement ID, use it for direct lookup + XapiStatementsResource.GetStatementParams( + statementId = statementIdUuid, + ) + } else { + // Otherwise, query by verb and activity + XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, activity = classActivityId, relatedActivities = true, - ), + ) + } + + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = listParams, dataLoadParams = DataLoadParams() ).collect { dataLoadState -> val statementResult = dataLoadState.dataOrNull() ?: return@collect diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 35629c446..07a1b1d14 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -278,7 +278,11 @@ class StudentGroupingEditViewModel( display = mapOf("en-US" to SAVED) ) + // Generate the statement ID before creating the statement + val statementId = Uuid.random() + val statement = XapiStatement( + id = statementId, actor = actor, verb = verb, `object` = group, @@ -300,7 +304,7 @@ class StudentGroupingEditViewModel( if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingDetail(groupId, route.classUid, statement.id?.toString()), + StudentGroupingDetail(groupId, route.classUid, statementId.toString()), popUpTo = route, popUpToInclusive = true ) From cdd5bc05dcafa8d489d1e27737e4006c20e8caab Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 18:24:09 +0530 Subject: [PATCH 050/130] Refactor xAPI statement retrieval and fix database DAO inconsistencies. - Simplify `loadGroupDetail` in `StudentGroupingDetailViewModel` to query by verb and activity only, removing direct lookup by statement ID. - Fix a bug in `XapiStatementEntityDao` and `XapiStatementEntityJsonDao` where `statementVerbId` was incorrectly used instead of `statementVerbUid`. - Clean up indentation and code formatting in `StudentGroupingEditViewModel`. - In `StudentGroupingEditViewModel`, remove manual statement ID generation and rely on the model-generated ID for navigation. - Remove unused `Uuid` import in `StudentGroupingDetailViewModel`. --- .../xapi/daos/XapiStatementEntityDao.kt | 2 +- .../xapi/daos/XapiStatementEntityJsonDao.kt | 2 +- .../detail/StudentGroupingDetailViewModel.kt | 20 +- .../edit/StudentGroupingEditViewModel.kt | 238 +++++++++--------- 4 files changed, 122 insertions(+), 140 deletions(-) diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt index aaeae5b10..defdaf789 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityDao.kt @@ -156,7 +156,7 @@ interface XapiStatementEntityDao { ) ) ) - AND (:verbUid = 0 OR XapiStatementEntity.statementVerbId = :verbUid) + AND (:verbUid = 0 OR XapiStatementEntity.statementVerbUid = :verbUid) -- Handle activity parameter AND ( :activityUid = 0 OR ( XapiStatementEntity.statementObjectType = ${XapiEntityObjectTypeFlags.ACTIVITY} diff --git a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt index 6f4214f36..c0377ce5d 100644 --- a/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt +++ b/respect-datalayer-db/src/commonMain/kotlin/world/respect/datalayer/db/school/xapi/daos/XapiStatementEntityJsonDao.kt @@ -129,7 +129,7 @@ interface XapiStatementEntityJsonDao { ) ) ) - AND (:verbUid = 0 OR XapiStatementEntity.statementVerbId = :verbUid) + AND (:verbUid = 0 OR XapiStatementEntity.statementVerbUid = :verbUid) -- Handle activity parameter AND ( :activityUid = 0 OR ( XapiStatementEntity.statementObjectType = ${XapiEntityObjectTypeFlags.ACTIVITY} diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 134912190..e7bd02fdc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -43,7 +43,6 @@ import kotlin.getValue import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid data class StudentGroupingDetailUiState( val selectedAccount: RespectSessionAndPerson? = null, @@ -170,25 +169,12 @@ class StudentGroupingDetailViewModel( @OptIn(ExperimentalUuidApi::class) private fun loadGroupDetail() { viewModelScope.launch { - // If we have a statementId, query by it directly, otherwise query by verb and activity - val statementIdUuid = route.statementId?.let { Uuid.parse(it) } - - val listParams = if (statementIdUuid != null) { - // When we have a specific statement ID, use it for direct lookup - XapiStatementsResource.GetStatementParams( - statementId = statementIdUuid, - ) - } else { - // Otherwise, query by verb and activity - XapiStatementsResource.GetStatementParams( + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, activity = classActivityId, relatedActivities = true, - ) - } - - schoolDataSource.xapiStatementsResource.getAsFlow( - listParams = listParams, + ), dataLoadParams = DataLoadParams() ).collect { dataLoadState -> val statementResult = dataLoadState.dataOrNull() ?: return@collect diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 07a1b1d14..3ee3f417a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -137,48 +137,48 @@ class StudentGroupingEditViewModel( @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { val statementResult = schoolDataSource.xapiStatementsResource.get( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_SAVED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull() ?: return@launch - - // Find the latest statement for this group by sorting by timestamp - val groupStatement = statementResult.statements - .filter { statement -> - val group = statement.`object` as? XapiGroup - group?.account?.name == groupId - } - .maxByOrNull { - it.timestamp ?: it.stored ?: Instant.DISTANT_PAST - } - ?: return@launch - - val group = groupStatement.`object` as XapiGroup - - val groupName = group.name - val statementId = groupStatement.id?.toString() - - val memberIds = group.member - ?.mapNotNull { it.account?.name } - ?: emptyList() - - val persons = memberIds.mapNotNull { id -> - schoolDataSource.personDataSource.findByGuid( - DataLoadParams(), - id - ).dataOrNull() + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull() ?: return@launch + + // Find the latest statement for this group by sorting by timestamp + val groupStatement = statementResult.statements + .filter { statement -> + val group = statement.`object` as? XapiGroup + group?.account?.name == groupId } - - _uiState.update { prev -> - prev.copy( - groupName = groupName ?: "", - selectedStudents = persons, - statementId = statementId - ) + .maxByOrNull { + it.timestamp ?: it.stored ?: Instant.DISTANT_PAST } + ?: return@launch + + val group = groupStatement.`object` as XapiGroup + + val groupName = group.name + val statementId = groupStatement.id?.toString() + + val memberIds = group.member + ?.mapNotNull { it.account?.name } + ?: emptyList() + + val persons = memberIds.mapNotNull { id -> + schoolDataSource.personDataSource.findByGuid( + DataLoadParams(), + id + ).dataOrNull() + } + + _uiState.update { prev -> + prev.copy( + groupName = groupName ?: "", + selectedStudents = persons, + statementId = statementId + ) + } } } viewModelScope.launch { @@ -207,111 +207,107 @@ class StudentGroupingEditViewModel( } viewModelScope.launch { - // If editing an existing group, void the old statement first - if (existingStatementId != null) { - - val actor = XapiAgent( - name = _uiState.value.personName, - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = _uiState.value.personId, - homePage = schoolSelfUrl.toString() - ) - ) + // If editing an existing group, void the old statement first + if (existingStatementId != null) { - val voidVerb = XapiVerb( - id = VERB_VOIDED, - display = mapOf("en-US" to VOIDED) - ) - - val statementRef = XapiStatementRef( - objectType = XapiObjectType.StatementRef, - id = existingStatementId - ) - - val voidingStatement = XapiStatement( - actor = actor, - verb = voidVerb, - `object` = statementRef, - timestamp = Clock.System.now() + val actor = XapiAgent( + name = _uiState.value.personName, + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = _uiState.value.personId, + homePage = schoolSelfUrl.toString() ) + ) - schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) - } - - // Create a new statement with the updated group information - val members = _uiState.value.selectedStudents.map { person -> - XapiAgent( - name = person.fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = person.guid, - homePage = schoolSelfUrl.toString() - ) - ) - } + val voidVerb = XapiVerb( + id = VERB_VOIDED, + display = mapOf("en-US" to VOIDED) + ) - val groupId = route.groupId ?: Uuid.random().toString() + val statementRef = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = existingStatementId + ) - val group = XapiGroup( - objectType = XapiObjectType.Group, - name = groupName, - account = XapiAccount( - name = groupId, - homePage = schoolSelfUrl.toString() - ), - member = members + val voidingStatement = XapiStatement( + actor = actor, + verb = voidVerb, + `object` = statementRef, + timestamp = Clock.System.now() ) - val actor = XapiAgent( - name = _uiState.value.personName, + schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + } + + // Create a new statement with the updated group information + val members = _uiState.value.selectedStudents.map { person -> + XapiAgent( + name = person.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( - name = _uiState.value.personId, + name = person.guid, homePage = schoolSelfUrl.toString() ) ) + } + val groupId = route.groupId ?: Uuid.random().toString() - val verb = XapiVerb( - id = VERB_SAVED, - display = mapOf("en-US" to SAVED) + val group = XapiGroup( + objectType = XapiObjectType.Group, + name = groupName, + account = XapiAccount( + name = groupId, + homePage = schoolSelfUrl.toString() + ), + member = members + ) + + val actor = XapiAgent( + name = _uiState.value.personName, + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = _uiState.value.personId, + homePage = schoolSelfUrl.toString() ) + ) - // Generate the statement ID before creating the statement - val statementId = Uuid.random() - val statement = XapiStatement( - id = statementId, - actor = actor, - verb = verb, - `object` = group, - timestamp = Clock.System.now(), - context = XapiContext( - contextActivities = XapiContextActivities( - parent = listOf( - XapiActivity( - id = classActivityId, - objectType = XapiObjectType.Activity - ) + val verb = XapiVerb( + id = VERB_SAVED, + display = mapOf("en-US" to SAVED) + ) + + val statement = XapiStatement( + actor = actor, + verb = verb, + `object` = group, + timestamp = Clock.System.now(), + context = XapiContext( + contextActivities = XapiContextActivities( + parent = listOf( + XapiActivity( + id = classActivityId, + objectType = XapiObjectType.Activity ) ) ) ) + ) - schoolDataSource.xapiStatementsResource.post(listOf(statement)) + schoolDataSource.xapiStatementsResource.post(listOf(statement)) - if (route.groupId == null) { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - StudentGroupingDetail(groupId, route.classUid, statementId.toString()), - popUpTo = route, - popUpToInclusive = true - ) + if (route.groupId == null) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + StudentGroupingDetail(groupId, route.classUid, statement.id?.toString()), + popUpTo = route, + popUpToInclusive = true ) - } else { - sendResultAndPop(RESULT_KEY_GROUP_UPDATED, true) - } + ) + } else { + sendResultAndPop(RESULT_KEY_GROUP_UPDATED, true) + } } } From aca36a84f1ecfd5ff2c8fea402a3cf24e2120013 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 6 May 2026 20:35:20 +0530 Subject: [PATCH 051/130] Clean up code formatting and simplify comments. - Remove unnecessary empty line in `StudentGroupingDetailScreen.kt`. - Simplify the comment explaining the xAPI statement sorting logic in `ClazzDetailViewModel.kt`. --- .../view/studentgrouping/detail/StudentGroupingDetailScreen.kt | 1 - .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 98e470c01..6cd3f60de 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -69,7 +69,6 @@ fun StudentGroupingDetailScreen( .fillMaxSize() ) { item { - ListItem( modifier = Modifier.clickable {}, leadingContent = { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 20a75a8d8..076d1314f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -428,8 +428,7 @@ class ClazzDetailViewModel( val statementResult = dataLoadState.dataOrNull() ?: return@collect - // Sort by timestamp descending (latest first) and take first per groupId - // This ensures we get the latest version of each group + // Sort by timestamp to get the latest version of each group val groupIdToStatement = statementResult.statements .filter { it.verb.id == VERB_SAVED } .sortedByDescending { it.timestamp ?: it.stored } From 0f831ad9493d387cf46c2a8fb465f6568b0da435 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 7 May 2026 15:13:45 +0530 Subject: [PATCH 052/130] Refactor `StudentGroupingDetailViewModel` and `StudentGroupingEditViewModel` to streamline account data handling and improve xAPI statement creation. - Remove `selectedAccount` and helper properties from `StudentGroupingDetailUiState` and `StudentGroupingEditUiState`. - Fetch the person data directly from `respectAccountManager.selectedAccountAndPersonFlow` when needed for xAPI statements. - Replace `viewModelScope.launch` with `launchWithLoadingIndicator` in `onConfirmDeleteGroup` and `onSaveGroup`. - Clean up the `StudentGroupingEditViewModel` initialization by removing the manual collection of the account flow. - Format navigation logic in `StudentGroupingEditViewModel` for better readability. --- .../detail/StudentGroupingDetailViewModel.kt | 24 ++++++------ .../edit/StudentGroupingEditViewModel.kt | 37 +++++++------------ 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index e7bd02fdc..cc3af9d4b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -6,6 +6,7 @@ import androidx.navigation.toRoute import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent @@ -37,7 +38,6 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS -import world.respect.shared.domain.account.RespectSessionAndPerson import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue import kotlin.time.Clock @@ -45,18 +45,11 @@ import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi data class StudentGroupingDetailUiState( - val selectedAccount: RespectSessionAndPerson? = null, val groupName: String = "", val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, val statementGroupId: String? = null, -) { - val personName: String - get() = selectedAccount?.person?.fullName() ?: "" - - val personId: String - get() = selectedAccount?.person?.guid ?: "" -} +) class StudentGroupingDetailViewModel( savedStateHandle: SavedStateHandle, @@ -124,20 +117,25 @@ class StudentGroupingDetailViewModel( fun onConfirmDeleteGroup() { _uiState.update { it.copy(showDeleteGroupDialog = false) } - viewModelScope.launch { + launchWithLoadingIndicator { + val statementId = _uiState.value.statementGroupId + if (statementId == null) { Napier.w("onConfirmDeleteGroup: Statement ID not found") - return@launch + return@launchWithLoadingIndicator } val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + val person = respectAccountManager.selectedAccountAndPersonFlow.first()?.person + ?: return@launchWithLoadingIndicator + val actor = XapiAgent( - name = _uiState.value.personName, + name = person.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( - name = _uiState.value.personId, + name = person.guid, homePage = schoolSelfUrl.toString() ) ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 3ee3f417a..0956b358d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent @@ -39,7 +40,6 @@ import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.domain.account.RespectSessionAndPerson import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit_group import world.respect.shared.generated.resources.create_group @@ -60,26 +60,19 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid data class StudentGroupingEditUiState( - val selectedAccount: RespectSessionAndPerson? = null, val groupName: String = "", val groupNameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), val selectedStudents: List = emptyList(), val statementId: String? = null ) { - val personName: String - get() = selectedAccount?.person?.fullName() ?: "" - - val personId: String - get() = selectedAccount?.person?.guid ?: "" - val selectedStudentIds: Set get() = selectedStudents.map { it.guid }.toSet() } class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, - respectAccountManager: RespectAccountManager + private val respectAccountManager: RespectAccountManager ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = respectAccountManager.requireActiveAccountScope() @@ -181,13 +174,6 @@ class StudentGroupingEditViewModel( } } } - viewModelScope.launch { - respectAccountManager.selectedAccountAndPersonFlow.collect { selectedAccount -> - _uiState.update { - it.copy(selectedAccount = selectedAccount) - } - } - } } @OptIn(ExperimentalUuidApi::class) @@ -205,16 +191,17 @@ class StudentGroupingEditViewModel( } else { _uiState.update { it.copy(groupNameError = null) } } + launchWithLoadingIndicator { + val person = respectAccountManager.selectedAccountAndPersonFlow.first()?.person + ?: return@launchWithLoadingIndicator - viewModelScope.launch { - // If editing an existing group, void the old statement first if (existingStatementId != null) { val actor = XapiAgent( - name = _uiState.value.personName, + name = person.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( - name = _uiState.value.personId, + name = person.guid, homePage = schoolSelfUrl.toString() ) ) @@ -264,10 +251,10 @@ class StudentGroupingEditViewModel( ) val actor = XapiAgent( - name = _uiState.value.personName, + name = person.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( - name = _uiState.value.personId, + name = person.guid, homePage = schoolSelfUrl.toString() ) ) @@ -300,7 +287,11 @@ class StudentGroupingEditViewModel( if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingDetail(groupId, route.classUid, statement.id?.toString()), + StudentGroupingDetail( + groupId, + route.classUid, + statement.id?.toString() + ), popUpTo = route, popUpToInclusive = true ) From b06e5a9c1367f33459a2e63ab62ffe1f3b9562dd Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 11 May 2026 16:46:04 +0530 Subject: [PATCH 053/130] flow 003 moved to disabled flow folder --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .maestro/{flows => flows-disable}/003_admin_user_assigns_assignment_to_a_class_test.yaml (100%) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows-disable/003_admin_user_assigns_assignment_to_a_class_test.yaml similarity index 100% rename from .maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml rename to .maestro/flows-disable/003_admin_user_assigns_assignment_to_a_class_test.yaml From 98c538d4013d2fb824a0b81e8b525357e1b0ef7c Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 13 May 2026 15:50:47 +0530 Subject: [PATCH 054/130] Merge dev xapi --- ...er_assigns_assignment_to_a_class_test.yaml | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 .maestro/flows-disable/003_admin_user_assigns_assignment_to_a_class_test.yaml diff --git a/.maestro/flows-disable/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows-disable/003_admin_user_assigns_assignment_to_a_class_test.yaml deleted file mode 100644 index b6bbe9c06..000000000 --- a/.maestro/flows-disable/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ /dev/null @@ -1,83 +0,0 @@ -appId: world.respect.app -onFlowStart: - - clearState: world.respect.app - - runScript: - file: "scripts/school_init.js" - env: - TESTCONTROLLER_URL: ${TESTCONTROLLER_URL} - SCHOOL_ADMIN_PASSWORD: ${SCHOOL_ADMIN_PASSWORD} - DIR_ADMIN_AUTH_HEADER: ${DIR_ADMIN_AUTH_HEADER} - SCHOOL_URL: ${SCHOOL_URL} - SCHOOL_NAME: ${SCHOOL_NAME} - URL_SUBSTITUTION: ${URL_SUBSTITUTION} - NAME: "003_admin_user_assigns_assignment_to_a_class_test" - -onFlowComplete: - - runScript: - file: "scripts/teardown.js" ---- -- runFlow: "subflows/admin_add_app_and_teacher.yaml" - -- clearState: world.respect.app -- launchApp: - arguments: - respect_directory: ${output.SCHOOL_URL} - -- tapOn: "Get Started" - -- runFlow: - file: "subflows/get_started_select_school_by_name.yaml" - env: - SCHOOL_NAME: ${SCHOOL_NAME} - -- tapOn: - id: "username" -- inputText: "teacheruser" -- tapOn: - id : "password" -- inputText: "testt1" -- tapOn: "Login" -- runFlow: - when: - visible: "Save password for Respect?" - file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" -- tapOn: "Classes" -- assertVisible: - id: "app_title" - text: "Classes" -- assertVisible: "TestClass" -- tapOn: "Assignments" -- tapOn: - id: "floating_action_button" # +Assignment button -- assertVisible: - id: "app_title" - text: "Add assignment" -- tapOn: "Save" -- assertVisible: "Required field" -- tapOn: "Assignment name*" -- inputText: "Homework 1" -- tapOn: "Class" -- tapOn: "TestClass" -- tapOn: "Date" -- runScript: - file: "scripts/setDate.js" -- inputText: ${output.futureDate} -- tapOn: "Time" -- runScript: - file: "scripts/setDate.js" -- inputText: ${output.currentTime} -- tapOn: "Lesson/assessment" -- assertVisible: - id: "app_title" -- tapOn: "My app" -- tapOn: "Grade 1" -- tapOn: "Lesson 001" -- tapOn: "Save" -- assertVisible: - id: "app_title" - text: "Homework 1" -- assertVisible: "Lesson 001" -- back -- assertVisible: "Homework 1" - From 1c2be5cfb15d36d6f8ae879154ae70144892782e Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 13 May 2026 15:53:21 +0530 Subject: [PATCH 055/130] Clean up unused imports and string resources in `ClazzDetailScreen.kt`. --- .../app/view/clazz/detail/ClazzDetailScreen.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 35f1ad3b1..7b11dd602 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -46,13 +46,11 @@ import world.respect.shared.generated.resources.pending_requests import world.respect.shared.generated.resources.collapse_pending_invites import world.respect.shared.generated.resources.collapse_students import world.respect.shared.generated.resources.collapse_teachers -import world.respect.shared.generated.resources.description import world.respect.shared.generated.resources.expand_pending_invites import world.respect.shared.generated.resources.expand_students import world.respect.shared.generated.resources.expand_teachers import world.respect.shared.generated.resources.manage_enrollments import world.respect.shared.generated.resources.more_options -import world.respect.shared.generated.resources.pending_requests import world.respect.shared.generated.resources.remove_from_class import world.respect.shared.generated.resources.student import world.respect.shared.generated.resources.students @@ -61,19 +59,8 @@ import world.respect.shared.generated.resources.teachers import world.respect.shared.util.SortOrderOption import world.respect.shared.viewmodel.clazz.detail.ClazzDetailUiState import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.IconButton -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.groups -import world.respect.shared.generated.resources.manage_enrollments -import world.respect.shared.generated.resources.more_options -import world.respect.shared.generated.resources.remove_from_class - @Composable fun ClazzDetailScreen( From cde8afe08eb19b0c097c5eb0b3ee82ed94afbd5e Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 14 May 2026 12:55:34 +0530 Subject: [PATCH 056/130] Clean up unused imports and string resources in `ClazzDetailScreen.kt`. --- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 8dbae53e7..c28678e1f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -98,8 +98,9 @@ data class ClazzDetailUiState( val isStudentGroupingExpanded: Boolean = true, val groupIds: List = emptyList(), val groups: List = emptyList(), - val statementId: String? = null -) { + val statementId: String? = null, + val addPersonPermissions: List = emptyList() + ) { fun showApproveOption(person: Person): Boolean { return person.roles.firstOrNull()?.let { From ef75ce6a0b78f71e1da40ea165ec962bb1f8468e Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 14 May 2026 13:53:59 +0530 Subject: [PATCH 057/130] Update the Maestro subflow for adding a new teacher to a class by removing redundant steps to select the user role. --- .maestro/flows/subflows/admin_add_new_teacher_to_class.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml b/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml index 8235c5f83..0e21f3e56 100644 --- a/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml +++ b/.maestro/flows/subflows/admin_add_new_teacher_to_class.yaml @@ -12,10 +12,6 @@ appId: world.respect.app - inputText: ${LASTNAME} - tapOn: "Gender*" - tapOn: "Female" -- tapOn: - id: "role" -- tapOn: - text: "Teacher" - tapOn: "Save" - assertVisible: text: ${FIRSTNAME} ${LASTNAME} From 3851c96810f424388bd3147b31d71550bfe6515a Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 14 May 2026 15:04:03 +0530 Subject: [PATCH 058/130] Relocate Maestro flow test files from `.maestro/flows/` to `.maestro/flows-test/`. Testing single test --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/{flows => flows-test}/001_002_add_user_direct_test.yaml | 0 .../001_003_login_using_school_link_test.yaml | 0 .../001_005_add_school_self_registration_test.yaml | 0 .maestro/{flows => flows-test}/002_browse_lessons_test.yaml | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/{flows => flows-test}/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/{flows => flows-test}/001_002_add_user_direct_test.yaml (100%) rename .maestro/{flows => flows-test}/001_003_login_using_school_link_test.yaml (100%) rename .maestro/{flows => flows-test}/001_005_add_school_self_registration_test.yaml (100%) rename .maestro/{flows => flows-test}/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows-test/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows-test/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows/001_002_add_user_direct_test.yaml b/.maestro/flows-test/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows/001_002_add_user_direct_test.yaml rename to .maestro/flows-test/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows/001_003_login_using_school_link_test.yaml b/.maestro/flows-test/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows/001_003_login_using_school_link_test.yaml rename to .maestro/flows-test/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows/001_005_add_school_self_registration_test.yaml b/.maestro/flows-test/001_005_add_school_self_registration_test.yaml similarity index 100% rename from .maestro/flows/001_005_add_school_self_registration_test.yaml rename to .maestro/flows-test/001_005_add_school_self_registration_test.yaml diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows-test/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows/002_browse_lessons_test.yaml rename to .maestro/flows-test/002_browse_lessons_test.yaml From 08f0bc6704852a5164543746b10806d21e6ad406 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 14 May 2026 15:13:38 +0530 Subject: [PATCH 059/130] Move Maestro flow test files from `.maestro/flows-test/` to `.maestro/flows/`. --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/{flows-test => flows}/001_002_add_user_direct_test.yaml | 0 .../001_003_login_using_school_link_test.yaml | 0 .../001_005_add_school_self_registration_test.yaml | 0 .maestro/{flows-test => flows}/002_browse_lessons_test.yaml | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/{flows-test => flows}/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/{flows-test => flows}/001_002_add_user_direct_test.yaml (100%) rename .maestro/{flows-test => flows}/001_003_login_using_school_link_test.yaml (100%) rename .maestro/{flows-test => flows}/001_005_add_school_self_registration_test.yaml (100%) rename .maestro/{flows-test => flows}/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows-test/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows-test/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows-test/001_002_add_user_direct_test.yaml b/.maestro/flows/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows-test/001_002_add_user_direct_test.yaml rename to .maestro/flows/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows-test/001_003_login_using_school_link_test.yaml b/.maestro/flows/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows-test/001_003_login_using_school_link_test.yaml rename to .maestro/flows/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows-test/001_005_add_school_self_registration_test.yaml b/.maestro/flows/001_005_add_school_self_registration_test.yaml similarity index 100% rename from .maestro/flows-test/001_005_add_school_self_registration_test.yaml rename to .maestro/flows/001_005_add_school_self_registration_test.yaml diff --git a/.maestro/flows-test/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows-test/002_browse_lessons_test.yaml rename to .maestro/flows/002_browse_lessons_test.yaml From 04cc63c27b525bc12798a8f897fccd841f451d71 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 09:32:35 +0530 Subject: [PATCH 060/130] Passing only groupId to the Student grouping detail . Throwing exception if the school Url is null/inappropriate Removed the Verb string --- .../view/clazz/detail/ClazzDetailScreen.kt | 23 +++++---- .../respect/shared/navigation/AppRoutes.kt | 4 +- .../clazz/detail/ClazzDetailViewModel.kt | 49 ++++-------------- .../detail/StudentGroupingDetailViewModel.kt | 51 +++++++++++-------- .../edit/StudentGroupingEditViewModel.kt | 34 +++++-------- .../world/respect/lib/xapi/model/XapiVerb.kt | 3 -- 6 files changed, 69 insertions(+), 95 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 7482674a3..0e4317609 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -58,6 +58,7 @@ import world.respect.shared.generated.resources.teacher import world.respect.shared.generated.resources.teachers import world.respect.shared.util.SortOrderOption import world.respect.shared.viewmodel.clazz.detail.ClazzDetailUiState +import world.respect.lib.xapi.model.XapiGroup import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.groups @@ -382,8 +383,8 @@ fun ClazzDetailScreen( headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), - text = if (uiState.groups.isNotEmpty()) - "${stringResource(Res.string.groups)} (${uiState.groups.size})" + text = if (uiState.groupStatements.isNotEmpty()) + "${stringResource(Res.string.groups)} (${uiState.groupStatements.size})" else stringResource(Res.string.groups) ) @@ -428,19 +429,23 @@ fun ClazzDetailScreen( ) } - items(uiState.groups.size) { index -> - val groupData = uiState.groups[index] + items(uiState.groupStatements.size) { index -> + val statement = uiState.groupStatements[index] + val group = statement.`object` as? XapiGroup + val groupId = group?.account?.name ?: return@items + val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + ListItem( modifier = Modifier .fillMaxWidth() .clickable { - onClickGroup(groupData.groupId) - }, + onClickGroup(groupId) + }, leadingContent = { Box( modifier = Modifier.size(40.dp), ) { - val displayMembers = groupData.memberNames.take(3) + val displayMembers = memberNames.take(3) displayMembers.forEachIndexed { i, name -> Box( modifier = Modifier @@ -456,10 +461,10 @@ fun ClazzDetailScreen( } }, headlineContent = { - Text(text = "${groupData.groupName} (${groupData.memberCount})") + Text(text = "${group.name} (${memberNames.size})") }, supportingContent = { - Text(text = "${groupData.memberCount} ${stringResource(Res.string.students)}") + Text(text = "${memberNames.size} ${stringResource(Res.string.students)}") } ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 14c7c22ff..f4fbcc586 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -227,9 +227,7 @@ data class StudentGroupingEdit( @Serializable data class StudentGroupingDetail( - val groupId: String, - val classId: String, - val statementId: String? = null + val groupId: String ): RespectAppRoute @Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 1c5447fa1..c06225a20 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -70,6 +70,7 @@ import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companio import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS @@ -96,9 +97,7 @@ data class ClazzDetailUiState( val showAddTeacher: Boolean = false, val showStudentGrouping: Boolean = false, val isStudentGroupingExpanded: Boolean = true, - val groupIds: List = emptyList(), - val groups: List = emptyList(), - val statementId: String? = null, + val groupStatements: List = emptyList(), val addPersonPermissions: List = emptyList() ) { @@ -109,13 +108,7 @@ data class ClazzDetailUiState( } } -data class GroupDisplayData( - val groupId: String, - val groupName: String, - val memberCount: Int, - val memberNames: List = emptyList(), - val statementId: String? = null -) + class ClazzDetailViewModel( savedStateHandle: SavedStateHandle, @@ -138,9 +131,10 @@ class ClazzDetailViewModel( private val route: ClazzDetail = savedStateHandle.toRoute() - val schoolSelfUrl = accountManager.activeAccount?.school?.self?.toString() + private val schoolSelfUrl: String = accountManager.activeAccount?.school?.self?.toString() + ?: throw IllegalStateException("schoolSelfUrl is required but activeAccount or school is null") - val classActivityId = "${schoolSelfUrl}${CLASS}${route.guid}" + private val classActivityId = "${schoolSelfUrl}${CLASS}${route.guid}" private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { @@ -357,16 +351,9 @@ class ClazzDetailViewModel( } fun onClickGroup(groupId: String) { - val groupData = _uiState.value.groups.find { it.groupId == groupId } - val statementId = groupData?.statementId - _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingDetail( - groupId = groupId, - classId = route.guid, - statementId = statementId - ) + StudentGroupingDetail(groupId = groupId) ) ) } @@ -455,8 +442,8 @@ class ClazzDetailViewModel( val statementResult = dataLoadState.dataOrNull() ?: return@collect - // Sort by timestamp to get the latest version of each group - val groupIdToStatement = statementResult.statements + // Sort by timestamp to get the latest version of each group, keep only the latest statement per group + val latestStatementPerGroup = statementResult.statements .filter { it.verb.id == VERB_SAVED } .sortedByDescending { it.timestamp ?: it.stored } .mapNotNull { statement -> @@ -469,24 +456,10 @@ class ClazzDetailViewModel( } } .distinctBy { it.first } - .toMap() - - val groupDisplayDataList = groupIdToStatement.map { (groupId, statement) -> - val group = statement.`object` as XapiGroup - val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - val statementId = statement.id?.toString() - - GroupDisplayData( - groupId = groupId, - groupName = group.name ?: "", - memberCount = memberNames.size, - memberNames = memberNames, - statementId = statementId - ) - } + .map { it.second } _uiState.update { prev -> - prev.copy(groups = groupDisplayDataList) + prev.copy(groupStatements = latestStatementPerGroup) } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index cc3af9d4b..752dc46f6 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -3,10 +3,9 @@ package world.respect.shared.viewmodel.studentgrouping.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent @@ -14,11 +13,10 @@ import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.db.school.ext.fullName -import world.respect.datalayer.ext.dataOrNull +import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.VERB_VOIDED -import world.respect.lib.xapi.model.VOIDED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiGroup @@ -49,6 +47,7 @@ data class StudentGroupingDetailUiState( val groupMembers: List = emptyList(), val showDeleteGroupDialog: Boolean = false, val statementGroupId: String? = null, + val classId: String? = null, ) class StudentGroupingDetailViewModel( @@ -67,9 +66,9 @@ class StudentGroupingDetailViewModel( private val route: StudentGroupingDetail = savedStateHandle.toRoute() - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self?.toString() + private val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + ?: throw IllegalStateException("schoolSelfUrl is required") - val classActivityId = "${schoolSelfUrl}${CLASS}${route.classId}" init { _appUiState.update { @@ -95,10 +94,12 @@ class StudentGroupingDetailViewModel( } fun onClickEdit() { + val classId = _uiState.value.classId + ?: throw IllegalStateException("classId not loaded when trying to edit group") _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingEdit( - classUid = route.classId, + classUid = classId, groupId = route.groupId ) ) @@ -120,16 +121,10 @@ class StudentGroupingDetailViewModel( launchWithLoadingIndicator { val statementId = _uiState.value.statementGroupId + ?: throw IllegalStateException("Statement ID not found when trying to delete group") - if (statementId == null) { - Napier.w("onConfirmDeleteGroup: Statement ID not found") - return@launchWithLoadingIndicator - } - - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self - - val person = respectAccountManager.selectedAccountAndPersonFlow.first()?.person - ?: return@launchWithLoadingIndicator + val person = respectAccountManager.selectedAccountAndPersonFlow.firstOrNull()?.person + ?: throw IllegalStateException("No person selected when trying to delete group") val actor = XapiAgent( name = person.fullName(), @@ -141,8 +136,7 @@ class StudentGroupingDetailViewModel( ) val verb = XapiVerb( - id = VERB_VOIDED, - display = mapOf("en-US" to VOIDED) + id = VERB_VOIDED ) val statementRef = XapiStatementRef( @@ -167,11 +161,19 @@ class StudentGroupingDetailViewModel( @OptIn(ExperimentalUuidApi::class) private fun loadGroupDetail() { viewModelScope.launch { + // Query by agent (the group itself) using its account identifier + val groupAgent = XapiAgent( + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = route.groupId, + homePage = schoolSelfUrl.toString() + ) + ) + schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, - activity = classActivityId, - relatedActivities = true, + agent = groupAgent, ), dataLoadParams = DataLoadParams() ).collect { dataLoadState -> @@ -189,11 +191,18 @@ class StudentGroupingDetailViewModel( val group = groupStatement.`object` as XapiGroup val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() val statementId = groupStatement.id?.toString() + + // Extract classId from context activities parent + val classActivityId = groupStatement.context?.contextActivities?.parent + ?.firstOrNull()?.id + val classId = classActivityId?.removePrefix("${schoolSelfUrl}${CLASS}") + _uiState.update { prev -> prev.copy( groupName = group.name ?: "", groupMembers = memberNames, - statementGroupId = statementId + statementGroupId = statementId, + classId = classId ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 0956b358d..acd162b97 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -13,7 +13,7 @@ import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.db.school.ext.fullName -import world.respect.datalayer.ext.dataOrNull +import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person @@ -21,10 +21,8 @@ import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.lib.dataloadstate.DataLoadParams -import world.respect.lib.xapi.model.SAVED import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.VERB_VOIDED -import world.respect.lib.xapi.model.VOIDED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiAgent @@ -55,7 +53,6 @@ import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import kotlin.getValue import kotlin.time.Clock import kotlin.time.Instant -import kotlin.toString import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -85,7 +82,8 @@ class StudentGroupingEditViewModel( private val route: StudentGroupingEdit = savedStateHandle.toRoute() - val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + private val schoolSelfUrl = respectAccountManager.activeAccount?.school?.self + ?: throw IllegalStateException("schoolSelfUrl is required") val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" @@ -152,7 +150,7 @@ class StudentGroupingEditViewModel( val group = groupStatement.`object` as XapiGroup val groupName = group.name - val statementId = groupStatement.id?.toString() + val statementId = groupStatement.id val memberIds = group.member ?.mapNotNull { it.account?.name } @@ -169,7 +167,7 @@ class StudentGroupingEditViewModel( prev.copy( groupName = groupName ?: "", selectedStudents = persons, - statementId = statementId + statementId = statementId.toString() ) } } @@ -180,7 +178,6 @@ class StudentGroupingEditViewModel( fun onClickSave() { val groupName = _uiState.value.groupName - val classActivityId = "${schoolSelfUrl}${CLASS}${route.classUid}" val existingStatementId = _uiState.value.statementId if (groupName.isBlank()) { @@ -192,8 +189,9 @@ class StudentGroupingEditViewModel( _uiState.update { it.copy(groupNameError = null) } } launchWithLoadingIndicator { + val person = respectAccountManager.selectedAccountAndPersonFlow.first()?.person - ?: return@launchWithLoadingIndicator + ?: throw IllegalStateException("No person selected when trying to save group") if (existingStatementId != null) { @@ -207,8 +205,7 @@ class StudentGroupingEditViewModel( ) val voidVerb = XapiVerb( - id = VERB_VOIDED, - display = mapOf("en-US" to VOIDED) + id = VERB_VOIDED ) val statementRef = XapiStatementRef( @@ -227,12 +224,12 @@ class StudentGroupingEditViewModel( } // Create a new statement with the updated group information - val members = _uiState.value.selectedStudents.map { person -> + val members = _uiState.value.selectedStudents.map { student -> XapiAgent( - name = person.fullName(), + name = student.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( - name = person.guid, + name = student.guid, homePage = schoolSelfUrl.toString() ) ) @@ -261,8 +258,7 @@ class StudentGroupingEditViewModel( val verb = XapiVerb( - id = VERB_SAVED, - display = mapOf("en-US" to SAVED) + id = VERB_SAVED ) val statement = XapiStatement( @@ -287,11 +283,7 @@ class StudentGroupingEditViewModel( if (route.groupId == null) { _navCommandFlow.tryEmit( NavCommand.Navigate( - StudentGroupingDetail( - groupId, - route.classUid, - statement.id?.toString() - ), + StudentGroupingDetail(groupId = groupId), popUpTo = route, popUpToInclusive = true ) diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index 889ecd232..1b87bb1ca 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -11,9 +11,6 @@ const val VERB_SAVED = "http://adlnet.gov/expapi/verbs/saved" const val VERB_VOIDED = "http://adlnet.gov/expapi/verbs/voided" -const val SAVED = "saved" - -const val VOIDED = "voided" @Serializable data class XapiVerb( From bae9575d2e7be4ea90fb1e26e3223bc4009c4da6 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 09:50:29 +0530 Subject: [PATCH 061/130] Update `ClazzDetailViewModel` and its UI to represent groups directly as `XapiGroup` objects rather than xAPI statements. --- .../view/clazz/detail/ClazzDetailScreen.kt | 18 +++--- .../detail/StudentGroupingDetailScreen.kt | 7 +-- .../clazz/detail/ClazzDetailViewModel.kt | 62 ++++++++++--------- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 0e4317609..24100bdb9 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -58,7 +59,6 @@ import world.respect.shared.generated.resources.teacher import world.respect.shared.generated.resources.teachers import world.respect.shared.util.SortOrderOption import world.respect.shared.viewmodel.clazz.detail.ClazzDetailUiState -import world.respect.lib.xapi.model.XapiGroup import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.groups @@ -383,8 +383,8 @@ fun ClazzDetailScreen( headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), - text = if (uiState.groupStatements.isNotEmpty()) - "${stringResource(Res.string.groups)} (${uiState.groupStatements.size})" + text = if (uiState.groups.isNotEmpty()) + "${stringResource(Res.string.groups)} (${uiState.groups.size})" else stringResource(Res.string.groups) ) @@ -429,10 +429,9 @@ fun ClazzDetailScreen( ) } - items(uiState.groupStatements.size) { index -> - val statement = uiState.groupStatements[index] - val group = statement.`object` as? XapiGroup - val groupId = group?.account?.name ?: return@items + items(uiState.groups.size) { index -> + val group = uiState.groups[index] + val groupId = group.account?.name ?: return@items val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() ListItem( @@ -443,7 +442,7 @@ fun ClazzDetailScreen( }, leadingContent = { Box( - modifier = Modifier.size(40.dp), + modifier = Modifier.width(40.dp), ) { val displayMembers = memberNames.take(3) displayMembers.forEachIndexed { i, name -> @@ -462,9 +461,6 @@ fun ClazzDetailScreen( }, headlineContent = { Text(text = "${group.name} (${memberNames.size})") - }, - supportingContent = { - Text(text = "${memberNames.size} ${stringResource(Res.string.students)}") } ) } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 6cd3f60de..1ecc596e7 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -70,11 +70,8 @@ fun StudentGroupingDetailScreen( ) { item { ListItem( - modifier = Modifier.clickable {}, leadingContent = { - Box( - modifier = Modifier.size(40.dp), - ) { + val displayMembers = uiState.groupMembers.take(3) displayMembers.forEachIndexed { i, name -> Box( @@ -88,7 +85,7 @@ fun StudentGroupingDetailScreen( ) } } - } + }, headlineContent = { val label = if (uiState.groupMembers.size > 1) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index c06225a20..193fcda62 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -75,10 +75,10 @@ import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS data class ClazzDetailUiState( - val teachers: IPagingSourceFactory = EmptyPagingSourceFactory() , + val teachers: IPagingSourceFactory = EmptyPagingSourceFactory(), val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val pendingTeachers:IPagingSourceFactory = EmptyPagingSourceFactory() , - val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory() , + val pendingTeachers: IPagingSourceFactory = EmptyPagingSourceFactory(), + val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory(), val listOfPending: List = emptyList(), val chipOptions: List = emptyList(), @@ -98,8 +98,9 @@ data class ClazzDetailUiState( val showStudentGrouping: Boolean = false, val isStudentGroupingExpanded: Boolean = true, val groupStatements: List = emptyList(), + val groups: List = emptyList(), val addPersonPermissions: List = emptyList() - ) { +) { fun showApproveOption(person: Person): Boolean { return person.roles.firstOrNull()?.let { @@ -149,9 +150,9 @@ class ClazzDetailViewModel( } } - private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) + private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) - private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) private val teachersPendingPagingSource = pagingSourceByRole(EnrollmentRoleEnum.PENDING_TEACHER) @@ -262,7 +263,7 @@ class ClazzDetailViewModel( ) ) ) - }catch(e: Throwable) { + } catch (e: Throwable) { e.printStackTrace() } } @@ -293,7 +294,7 @@ class ClazzDetailViewModel( ) ) ) - } + } } fun onSortOrderChanged(sortOption: SortOrderOption) { @@ -379,9 +380,9 @@ class ClazzDetailViewModel( }.map { it.copy( lastModified = modTime, - status = if(it.beginDate == today) { + status = if (it.beginDate == today) { StatusEnum.TO_BE_DELETED //probably was just added by mistake - }else { + } else { it.status }, endDate = today, @@ -391,7 +392,7 @@ class ClazzDetailViewModel( schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) - }catch(e: Throwable) { + } catch (e: Throwable) { //do something Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) @@ -440,27 +441,28 @@ class ClazzDetailViewModel( dataLoadParams = DataLoadParams() ).collect { dataLoadState -> - val statementResult = dataLoadState.dataOrNull() ?: return@collect - - // Sort by timestamp to get the latest version of each group, keep only the latest statement per group - val latestStatementPerGroup = statementResult.statements - .filter { it.verb.id == VERB_SAVED } - .sortedByDescending { it.timestamp ?: it.stored } - .mapNotNull { statement -> - val group = statement.`object` as? XapiGroup - val groupId = group?.account?.name - if (groupId != null) { - groupId to statement - } else { - null - } + val statementResult = dataLoadState.dataOrNull() ?: return@collect + + // Sort by timestamp to get the latest version of each group, keep only the latest statement per group + val latestGroups = statementResult.statements + .filter { it.verb.id == VERB_SAVED } + .sortedByDescending { it.timestamp ?: it.stored } + .mapNotNull { statement -> + val group = statement.`object` as? XapiGroup + val groupId = group?.account?.name + if (groupId != null) { + groupId to group + } else { + null } - .distinctBy { it.first } - .map { it.second } - - _uiState.update { prev -> - prev.copy(groupStatements = latestStatementPerGroup) } + .distinctBy { it.first } + .map { it.second } + + _uiState.update { prev -> + prev.copy(groups = latestGroups) + } + } } From 2f87259c2279d55f85a9dbb875c6eba01ecec319 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 10:15:17 +0530 Subject: [PATCH 062/130] Update UI and state management for student groupings in `ClazzDetailScreen` and `StudentGroupingDetailScreen`. --- .../respect/app/view/clazz/detail/ClazzDetailScreen.kt | 6 ++++-- .../studentgrouping/detail/StudentGroupingDetailScreen.kt | 5 +++-- .../src/commonMain/composeResources/values/strings.xml | 2 ++ .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 2 -- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 24100bdb9..58bc2334a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -47,9 +47,11 @@ import world.respect.shared.generated.resources.pending_requests import world.respect.shared.generated.resources.collapse_pending_invites import world.respect.shared.generated.resources.collapse_students import world.respect.shared.generated.resources.collapse_teachers +import world.respect.shared.generated.resources.collapse_groups import world.respect.shared.generated.resources.expand_pending_invites import world.respect.shared.generated.resources.expand_students import world.respect.shared.generated.resources.expand_teachers +import world.respect.shared.generated.resources.expand_groups import world.respect.shared.generated.resources.manage_enrollments import world.respect.shared.generated.resources.more_options import world.respect.shared.generated.resources.remove_from_class @@ -394,9 +396,9 @@ fun ClazzDetailScreen( Icon( imageVector = Icons.Outlined.KeyboardArrowDown, contentDescription = if (uiState.isStudentGroupingExpanded) { - stringResource(Res.string.collapse_students) + stringResource(Res.string.collapse_groups) } else { - stringResource(Res.string.expand_students) + stringResource(Res.string.expand_groups) }, modifier = Modifier.size(24.dp) .rotate( diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt index 1ecc596e7..2d5a1fdb2 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/detail/StudentGroupingDetailScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete @@ -71,7 +72,7 @@ fun StudentGroupingDetailScreen( item { ListItem( leadingContent = { - + Box(modifier = Modifier.width(40.dp)) { val displayMembers = uiState.groupMembers.take(3) displayMembers.forEachIndexed { i, name -> Box( @@ -85,7 +86,7 @@ fun StudentGroupingDetailScreen( ) } } - + } }, headlineContent = { val label = if (uiState.groupMembers.size > 1) diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 36b33dffe..d560b5d80 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -425,6 +425,8 @@ Expand students list Collapse students list + Expand groups list + Collapse groups list Add person Select person diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 193fcda62..85061a8a3 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -70,7 +70,6 @@ import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companio import kotlin.time.Clock import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiGroup -import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS @@ -97,7 +96,6 @@ data class ClazzDetailUiState( val showAddTeacher: Boolean = false, val showStudentGrouping: Boolean = false, val isStudentGroupingExpanded: Boolean = true, - val groupStatements: List = emptyList(), val groups: List = emptyList(), val addPersonPermissions: List = emptyList() ) { From 49d5618d2324fed67944714a6c66c21b899f58de Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 10:46:07 +0530 Subject: [PATCH 063/130] Refactor `ClazzDetailScreen.kt` and `StudentGroupingEditScreen.kt` to improve list performance and error handling. --- .../app/view/clazz/detail/ClazzDetailScreen.kt | 15 +++++++++++---- .../edit/StudentGroupingEditScreen.kt | 7 ++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 58bc2334a..5a103cf4a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.MoreVert @@ -431,9 +432,15 @@ fun ClazzDetailScreen( ) } - items(uiState.groups.size) { index -> - val group = uiState.groups[index] - val groupId = group.account?.name ?: return@items + items( + items = uiState.groups, + key = { group -> + group.account?.name + ?: throw IllegalStateException("Group id should not be null") + } + ) { group -> + val groupId = group.account?.name + ?: throw IllegalStateException("Group id should not be null") val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() ListItem( @@ -441,7 +448,7 @@ fun ClazzDetailScreen( .fillMaxWidth() .clickable { onClickGroup(groupId) - }, + }, leadingContent = { Box( modifier = Modifier.width(40.dp), diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 710cce4a0..23efee65d 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -21,7 +21,6 @@ import world.respect.app.components.respectPagingItems import world.respect.app.components.respectRememberPager import world.respect.app.components.uiTextStringResource import world.respect.datalayer.db.school.ext.fullName -import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.name @@ -93,8 +92,10 @@ fun StudentGroupingEditScreen( person = student, isSelected = isSelected, onCheckedChange = { checked -> - student?.let { - onStudentCheckedChange(it, checked) + if (student != null) { + onStudentCheckedChange(student, checked) + } else { + throw IllegalStateException("Student should not be null when checkbox is changed") } } ) From 07a352d6586dff8e2b0d34bafa70bf5de9554551 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 10:47:20 +0530 Subject: [PATCH 064/130] Refactor `ClazzDetailScreen.kt` and `StudentGroupingEditScreen.kt` to improve list performance and error handling. --- .../view/studentgrouping/edit/StudentGroupingEditScreen.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 23efee65d..61325a03c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -92,10 +92,8 @@ fun StudentGroupingEditScreen( person = student, isSelected = isSelected, onCheckedChange = { checked -> - if (student != null) { - onStudentCheckedChange(student, checked) - } else { - throw IllegalStateException("Student should not be null when checkbox is changed") + student?.let { + onStudentCheckedChange(it, checked) } } ) From 8d45259b6908057fd1ddebf6773b3d636fe4fa8f Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 12:31:49 +0530 Subject: [PATCH 065/130] Refactor `ClazzDetailScreen.kt` and `ClazzDetailViewModel.kt` to improve group ID validation and filter out groups with null account names. --- .../app/view/clazz/detail/ClazzDetailScreen.kt | 15 +++++++++------ .../composeResources/values/strings.xml | 2 +- .../clazz/detail/ClazzDetailViewModel.kt | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 5a103cf4a..83e0abe29 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -382,7 +382,8 @@ fun ClazzDetailScreen( item("student_grouping_header") { ListItem( - modifier = Modifier.clickable { onToggleStudentGroupingSection() }, + modifier = Modifier + .clickable { onToggleStudentGroupingSection() }, headlineContent = { Text( modifier = Modifier.padding(top = 24.dp), @@ -435,13 +436,14 @@ fun ClazzDetailScreen( items( items = uiState.groups, key = { group -> - group.account?.name - ?: throw IllegalStateException("Group id should not be null") + requireNotNull(group.account?.name) } ) { group -> - val groupId = group.account?.name - ?: throw IllegalStateException("Group id should not be null") - val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() + val groupId = requireNotNull(group.account?.name) + + val memberNames = group.member + ?.mapNotNull { it.name } + ?: emptyList() ListItem( modifier = Modifier @@ -475,6 +477,7 @@ fun ClazzDetailScreen( } } } + } } diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index d560b5d80..0feeb6253 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -143,7 +143,7 @@ Add Teacher"> Add Student"> Add Student to create a Group"> - Create Group"> + Create group"> Edit Group"> Groups"> Accept Invite"> diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 85061a8a3..e4c8362c5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -456,6 +456,7 @@ class ClazzDetailViewModel( } .distinctBy { it.first } .map { it.second } + .filter { it.account?.name != null } _uiState.update { prev -> prev.copy(groups = latestGroups) From 7ed76169eeb9387064c7f35410467cbe364a0a2d Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 14:35:47 +0530 Subject: [PATCH 066/130] Update `ClazzDetailViewModel` to include groups with null account names and update Maestro flow assertions to use "Example Web App" and "Examples" labels. --- .maestro/flows/002_browse_lessons_test.yaml | 8 ++++---- .maestro/flows/subflows/admin_add_app.yaml | 3 +-- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index ccfc227b5..4048f0db1 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -26,7 +26,7 @@ onFlowComplete: - assertVisible: id: "app_title" text: "App detail" -- assertVisible: "My app" +- assertVisible: "Example Web App" - assertVisible: "Add App" # verify App got added to Apps section - tapOn: "Add App" @@ -34,11 +34,11 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Apps" -- assertVisible: "My app" -- tapOn: "My app" +- assertVisible: "Example Web App" +- tapOn: "Example Web App" - assertVisible: "Lessons" - tapOn: "Lessons" -- tapOn: "Grade 1" +- tapOn: "Examples" - tapOn: "Lesson 001" - assertVisible: "Lesson 001" - assertVisible: "App name" diff --git a/.maestro/flows/subflows/admin_add_app.yaml b/.maestro/flows/subflows/admin_add_app.yaml index b5dee3870..c57525736 100644 --- a/.maestro/flows/subflows/admin_add_app.yaml +++ b/.maestro/flows/subflows/admin_add_app.yaml @@ -14,5 +14,4 @@ appId: world.respect.app id: "app_title" text: "App detail" - tapOn: "Add App" -- tapOn: "Apps" -- assertVisible: "My app" \ No newline at end of file +- tapOn: "Apps" \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index e4c8362c5..85061a8a3 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -456,7 +456,6 @@ class ClazzDetailViewModel( } .distinctBy { it.first } .map { it.second } - .filter { it.account?.name != null } _uiState.update { prev -> prev.copy(groups = latestGroups) From c6dee1e5ab71a4595e2568c6d29ac442826472b6 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 14:48:06 +0530 Subject: [PATCH 067/130] Refactor `ClazzDetailViewModel.kt` to clean up formatting and whitespace. --- .../viewmodel/clazz/detail/ClazzDetailViewModel.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 85061a8a3..9aef88ef2 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -261,13 +261,12 @@ class ClazzDetailViewModel( ) ) ) - } catch (e: Throwable) { + }catch(e: Throwable) { e.printStackTrace() } } } } - } fun onClickAddPersonToClazz(roleType: EnrollmentRoleEnum) { @@ -378,9 +377,9 @@ class ClazzDetailViewModel( }.map { it.copy( lastModified = modTime, - status = if (it.beginDate == today) { + status = if(it.beginDate == today) { StatusEnum.TO_BE_DELETED //probably was just added by mistake - } else { + }else { it.status }, endDate = today, @@ -390,7 +389,7 @@ class ClazzDetailViewModel( schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) - } catch (e: Throwable) { + }catch(e: Throwable) { //do something Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) From a93d8f58578f169f9b72317be29708607c910dcf Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 16:29:47 +0530 Subject: [PATCH 068/130] Introduce `XapiStatementExt.kt` to centralize xAPI helper functions and refactor student grouping ViewModels. --- .../edit/StudentGroupingEditScreen.kt | 14 ++-- .../shared/domain/xapi/XapiStatementExt.kt | 71 +++++++++++++++++++ .../detail/StudentGroupingDetailViewModel.kt | 47 ++---------- .../edit/StudentGroupingEditViewModel.kt | 57 ++------------- 4 files changed, 93 insertions(+), 96 deletions(-) create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 61325a03c..460cf567a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -86,10 +86,14 @@ fun StudentGroupingEditScreen( } ) { student -> - val isSelected = student?.guid in uiState.selectedStudentIds + val person = checkNotNull(student) { + "Student item should not be null" + } + + val isSelected = student.guid in uiState.selectedStudentIds PersonListItemWithMenu( - person = student, + person = person, isSelected = isSelected, onCheckedChange = { checked -> student?.let { @@ -103,7 +107,7 @@ fun StudentGroupingEditScreen( @Composable fun PersonListItemWithMenu( - person: Person?, + person: Person, isSelected: Boolean, onCheckedChange: (Boolean) -> Unit ) { @@ -114,10 +118,10 @@ fun PersonListItemWithMenu( onCheckedChange(!isSelected) }, leadingContent = { - RespectPersonAvatar(name = person?.fullName() ?: "") + RespectPersonAvatar(name = person.fullName()) }, headlineContent = { - Text(text = person?.fullName().orEmpty()) + Text(text = person.fullName()) }, trailingContent = { Checkbox( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt new file mode 100644 index 000000000..192375ee1 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt @@ -0,0 +1,71 @@ +package world.respect.shared.domain.xapi + +import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.db.school.ext.fullName +import world.respect.datalayer.school.model.Person +import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiAgent +import world.respect.lib.xapi.model.XapiObjectType +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiStatementRef +import world.respect.lib.xapi.model.XapiVerb +import kotlin.time.Clock +import kotlin.uuid.ExperimentalUuidApi + +/** + * Extension function to convert a Person to an XapiAgent with the school's home page. + * + * @param schoolSelfUrl The URL of the school to use as the account home page + * @return An XapiAgent representing this person + */ +fun Person.toXapiAgent(schoolSelfUrl: String): XapiAgent { + return XapiAgent( + name = fullName(), + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = guid, + homePage = schoolSelfUrl + ) + ) +} + +/** + * Create an XapiAgent for querying purposes using an identifier and school URL. + * + * @param identifier The identifier (e.g., group ID) to use as the account name + * @param schoolSelfUrl The URL of the school to use as the account home page + * @return An XapiAgent for querying + */ +fun createXapiAgentForQuery(identifier: String, schoolSelfUrl: String): XapiAgent { + return XapiAgent( + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = identifier, + homePage = schoolSelfUrl + ) + ) +} + +/** + * Create a voiding statement for an existing xAPI statement. + * + * @param actor The actor performing the voiding action + * @param statementId The ID of the statement to void + * @return An XapiStatement that voids the specified statement + */ +@OptIn(ExperimentalUuidApi::class) +fun createVoidingStatement(actor: XapiAgent, statementId: String): XapiStatement { + return XapiStatement( + actor = actor, + verb = XapiVerb(id = VERB_VOIDED), + `object` = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = statementId + ), + timestamp = Clock.System.now() + ) +} + + + diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 752dc46f6..3b11091c6 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -12,20 +12,14 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.db.school.ext.fullName import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED -import world.respect.lib.xapi.model.VERB_VOIDED -import world.respect.lib.xapi.model.XapiAccount -import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiGroup -import world.respect.lib.xapi.model.XapiObjectType -import world.respect.lib.xapi.model.XapiStatement -import world.respect.lib.xapi.model.XapiStatementRef -import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.xapi.createXapiAgentForQuery +import world.respect.shared.domain.xapi.toXapiAgent import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit import world.respect.shared.navigation.NavCommand @@ -36,9 +30,9 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS +import world.respect.shared.domain.xapi.createVoidingStatement import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue -import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi @@ -126,33 +120,10 @@ class StudentGroupingDetailViewModel( val person = respectAccountManager.selectedAccountAndPersonFlow.firstOrNull()?.person ?: throw IllegalStateException("No person selected when trying to delete group") - val actor = XapiAgent( - name = person.fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = person.guid, - homePage = schoolSelfUrl.toString() - ) - ) - - val verb = XapiVerb( - id = VERB_VOIDED - ) - - val statementRef = XapiStatementRef( - objectType = XapiObjectType.StatementRef, - id = statementId - ) - - val voidingStatement = XapiStatement( - actor = actor, - verb = verb, - `object` = statementRef, - timestamp = Clock.System.now() - ) + val actor = person.toXapiAgent(schoolSelfUrl.toString()) + val voidingStatement = createVoidingStatement(actor, statementId) schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) - _navCommandFlow.tryEmit(NavCommand.PopUp()) } @@ -162,13 +133,7 @@ class StudentGroupingDetailViewModel( private fun loadGroupDetail() { viewModelScope.launch { // Query by agent (the group itself) using its account identifier - val groupAgent = XapiAgent( - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = route.groupId, - homePage = schoolSelfUrl.toString() - ) - ) + val groupAgent = createXapiAgentForQuery(route.groupId, schoolSelfUrl.toString()) schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index acd162b97..b9e1651a0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -12,7 +12,6 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.db.school.ext.fullName import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.EnrollmentRoleEnum @@ -22,10 +21,8 @@ import world.respect.datalayer.shared.paging.IPagingSourceFactory import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED -import world.respect.lib.xapi.model.VERB_VOIDED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity -import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiContext import world.respect.lib.xapi.model.XapiContextActivities import world.respect.lib.xapi.model.XapiGroup @@ -33,11 +30,12 @@ import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiObjectType import world.respect.lib.xapi.model.XapiStatement -import world.respect.lib.xapi.model.XapiStatementRef import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.xapi.createVoidingStatement +import world.respect.shared.domain.xapi.toXapiAgent import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit_group import world.respect.shared.generated.resources.create_group @@ -51,7 +49,6 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import kotlin.getValue -import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -193,46 +190,16 @@ class StudentGroupingEditViewModel( val person = respectAccountManager.selectedAccountAndPersonFlow.first()?.person ?: throw IllegalStateException("No person selected when trying to save group") - if (existingStatementId != null) { - - val actor = XapiAgent( - name = person.fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = person.guid, - homePage = schoolSelfUrl.toString() - ) - ) - - val voidVerb = XapiVerb( - id = VERB_VOIDED - ) - - val statementRef = XapiStatementRef( - objectType = XapiObjectType.StatementRef, - id = existingStatementId - ) - - val voidingStatement = XapiStatement( - actor = actor, - verb = voidVerb, - `object` = statementRef, - timestamp = Clock.System.now() - ) + val actor = person.toXapiAgent(schoolSelfUrl.toString()) + if (existingStatementId != null) { + val voidingStatement = createVoidingStatement(actor, existingStatementId) schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) } // Create a new statement with the updated group information val members = _uiState.value.selectedStudents.map { student -> - XapiAgent( - name = student.fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = student.guid, - homePage = schoolSelfUrl.toString() - ) - ) + student.toXapiAgent(schoolSelfUrl.toString()) } val groupId = route.groupId ?: Uuid.random().toString() @@ -247,16 +214,6 @@ class StudentGroupingEditViewModel( member = members ) - val actor = XapiAgent( - name = person.fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = person.guid, - homePage = schoolSelfUrl.toString() - ) - ) - - val verb = XapiVerb( id = VERB_SAVED ) @@ -265,7 +222,7 @@ class StudentGroupingEditViewModel( actor = actor, verb = verb, `object` = group, - timestamp = Clock.System.now(), + timestamp = kotlin.time.Clock.System.now(), context = XapiContext( contextActivities = XapiContextActivities( parent = listOf( From a47996dd06f5d2dcd1a6235ca362a94347fdaf01 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 19 May 2026 16:43:13 +0530 Subject: [PATCH 069/130] Remove unused `SchoolDataSource` import in `XapiStatementExt.kt`. --- .../kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt index 192375ee1..efb625110 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt @@ -1,6 +1,5 @@ package world.respect.shared.domain.xapi -import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.db.school.ext.fullName import world.respect.datalayer.school.model.Person import world.respect.lib.xapi.model.VERB_VOIDED From fd555e517d67bb2433e8ab75b059c60ba5b8cf77 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 20 May 2026 15:01:45 +0530 Subject: [PATCH 070/130] Remove unused `SchoolDataSource` import in `XapiStatementExt.kt`. --- .../edit/StudentGroupingEditScreen.kt | 6 ++--- .../clazz/detail/ClazzDetailViewModel.kt | 16 ++++++++------ .../detail/StudentGroupingDetailViewModel.kt | 14 +++++++----- .../edit/StudentGroupingEditViewModel.kt | 22 ++++++++++++------- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 460cf567a..959b15b8d 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -90,15 +90,13 @@ fun StudentGroupingEditScreen( "Student item should not be null" } - val isSelected = student.guid in uiState.selectedStudentIds + val isSelected = person.guid in uiState.selectedStudentIds PersonListItemWithMenu( person = person, isSelected = isSelected, onCheckedChange = { checked -> - student?.let { - onStudentCheckedChange(it, checked) - } + onStudentCheckedChange(person, checked) } ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 9aef88ef2..b4af86fa8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -441,17 +441,19 @@ class ClazzDetailViewModel( val statementResult = dataLoadState.dataOrNull() ?: return@collect // Sort by timestamp to get the latest version of each group, keep only the latest statement per group + val latestGroups = statementResult.statements .filter { it.verb.id == VERB_SAVED } .sortedByDescending { it.timestamp ?: it.stored } - .mapNotNull { statement -> - val group = statement.`object` as? XapiGroup - val groupId = group?.account?.name - if (groupId != null) { - groupId to group - } else { - null + .map { statement -> + val group = checkNotNull(statement.`object` as? XapiGroup) { + "Expected XapiGroup object in statement ${statement.id}" + } + val groupId = checkNotNull(group.account?.name) { + "Group account name missing in statement ${statement.id}" } + + groupId to group } .distinctBy { it.first } .map { it.second } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 3b11091c6..1ba617605 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -155,25 +155,29 @@ class StudentGroupingDetailViewModel( if (groupStatement != null) { val group = groupStatement.`object` as XapiGroup val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - val statementId = groupStatement.id?.toString() + val statementId = checkNotNull(groupStatement.id) { + "Group statement id should not be null" + } + val groupName = checkNotNull(group.name) { + "Group name should not be null" + } - // Extract classId from context activities parent val classActivityId = groupStatement.context?.contextActivities?.parent ?.firstOrNull()?.id val classId = classActivityId?.removePrefix("${schoolSelfUrl}${CLASS}") _uiState.update { prev -> prev.copy( - groupName = group.name ?: "", + groupName = groupName, groupMembers = memberNames, - statementGroupId = statementId, + statementGroupId = statementId.toString(), classId = classId ) } _appUiState.update { it.copy( - title = group.name?.asUiText() ?: "".asUiText() + title = group.name?.asUiText() ) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index b9e1651a0..43503ec62 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -58,11 +58,9 @@ data class StudentGroupingEditUiState( val groupNameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), val selectedStudents: List = emptyList(), + val selectedStudentIds: Set = emptySet(), val statementId: String? = null -) { - val selectedStudentIds: Set - get() = selectedStudents.map { it.guid }.toSet() -} +) class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, @@ -131,9 +129,11 @@ class StudentGroupingEditViewModel( relatedActivities = true, ), dataLoadParams = DataLoadParams() - ).dataOrNull() ?: return@launch + ).dataOrNull() + ?: throw IllegalStateException( + "Failed to load statements for classActivityId=$classActivityId" + ) - // Find the latest statement for this group by sorting by timestamp val groupStatement = statementResult.statements .filter { statement -> val group = statement.`object` as? XapiGroup @@ -142,7 +142,9 @@ class StudentGroupingEditViewModel( .maxByOrNull { it.timestamp ?: it.stored ?: Instant.DISTANT_PAST } - ?: return@launch + ?: throw IllegalStateException( + "Could not find statement for groupId=$groupId" + ) val group = groupStatement.`object` as XapiGroup @@ -164,6 +166,7 @@ class StudentGroupingEditViewModel( prev.copy( groupName = groupName ?: "", selectedStudents = persons, + selectedStudentIds = persons.map { it.guid }.toSet(), statementId = statementId.toString() ) } @@ -261,7 +264,10 @@ class StudentGroupingEditViewModel( prev.selectedStudents.filterNot { it.guid == person.guid } } - prev.copy(selectedStudents = updated) + prev.copy( + selectedStudents = updated, + selectedStudentIds = updated.map { it.guid }.toSet() + ) } } From e686becd21c74ffff565a458fe7d198a258e71e8 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 20 May 2026 15:19:06 +0530 Subject: [PATCH 071/130] Update group filtering logic in `ClazzDetailViewModel.kt` to handle null timestamps and ensure distinct groups are correctly mapped from xAPI statements. --- .../clazz/detail/ClazzDetailViewModel.kt | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index b4af86fa8..5e3a0045b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -72,6 +72,7 @@ import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS +import kotlin.time.Instant data class ClazzDetailUiState( val teachers: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -429,6 +430,7 @@ class ClazzDetailViewModel( @OptIn(kotlin.uuid.ExperimentalUuidApi::class) private suspend fun observeGroupsFromXapi() { + schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, @@ -438,30 +440,34 @@ class ClazzDetailViewModel( dataLoadParams = DataLoadParams() ).collect { dataLoadState -> - val statementResult = dataLoadState.dataOrNull() ?: return@collect - - // Sort by timestamp to get the latest version of each group, keep only the latest statement per group + val statementResult = dataLoadState.dataOrNull() + ?: return@collect val latestGroups = statementResult.statements - .filter { it.verb.id == VERB_SAVED } - .sortedByDescending { it.timestamp ?: it.stored } + .filter { statement -> + statement.verb.id == VERB_SAVED + } + .sortedByDescending { statement -> + statement.timestamp + ?: statement.stored + ?: Instant.DISTANT_PAST + } .map { statement -> - val group = checkNotNull(statement.`object` as? XapiGroup) { + + checkNotNull(statement.`object` as? XapiGroup) { "Expected XapiGroup object in statement ${statement.id}" } - val groupId = checkNotNull(group.account?.name) { - "Group account name missing in statement ${statement.id}" - } + } + .distinctBy { group -> - groupId to group + checkNotNull(group.account?.name) { + "Group account name missing" + } } - .distinctBy { it.first } - .map { it.second } _uiState.update { prev -> prev.copy(groups = latestGroups) } - } } From ebd422f539efa51cb39987b2eb0cc9b11acce039 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 20 May 2026 17:19:02 +0530 Subject: [PATCH 072/130] Remove `XapiStatementExt.kt` and inline xAPI agent and statement creation logic within `StudentGroupingDetailViewModel` and `StudentGroupingEditViewModel`. Use `xapiAgent` directly from the selected account session and manually construct `XapiStatement` and `XapiAgent` objects for group management and voiding operations. --- .../shared/domain/xapi/XapiStatementExt.kt | 70 ------------------- .../detail/StudentGroupingDetailViewModel.kt | 33 +++++++-- .../edit/StudentGroupingEditViewModel.kt | 33 +++++++-- 3 files changed, 52 insertions(+), 84 deletions(-) delete mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt deleted file mode 100644 index efb625110..000000000 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiStatementExt.kt +++ /dev/null @@ -1,70 +0,0 @@ -package world.respect.shared.domain.xapi - -import world.respect.datalayer.db.school.ext.fullName -import world.respect.datalayer.school.model.Person -import world.respect.lib.xapi.model.VERB_VOIDED -import world.respect.lib.xapi.model.XapiAccount -import world.respect.lib.xapi.model.XapiAgent -import world.respect.lib.xapi.model.XapiObjectType -import world.respect.lib.xapi.model.XapiStatement -import world.respect.lib.xapi.model.XapiStatementRef -import world.respect.lib.xapi.model.XapiVerb -import kotlin.time.Clock -import kotlin.uuid.ExperimentalUuidApi - -/** - * Extension function to convert a Person to an XapiAgent with the school's home page. - * - * @param schoolSelfUrl The URL of the school to use as the account home page - * @return An XapiAgent representing this person - */ -fun Person.toXapiAgent(schoolSelfUrl: String): XapiAgent { - return XapiAgent( - name = fullName(), - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = guid, - homePage = schoolSelfUrl - ) - ) -} - -/** - * Create an XapiAgent for querying purposes using an identifier and school URL. - * - * @param identifier The identifier (e.g., group ID) to use as the account name - * @param schoolSelfUrl The URL of the school to use as the account home page - * @return An XapiAgent for querying - */ -fun createXapiAgentForQuery(identifier: String, schoolSelfUrl: String): XapiAgent { - return XapiAgent( - objectType = XapiObjectType.Agent, - account = XapiAccount( - name = identifier, - homePage = schoolSelfUrl - ) - ) -} - -/** - * Create a voiding statement for an existing xAPI statement. - * - * @param actor The actor performing the voiding action - * @param statementId The ID of the statement to void - * @return An XapiStatement that voids the specified statement - */ -@OptIn(ExperimentalUuidApi::class) -fun createVoidingStatement(actor: XapiAgent, statementId: String): XapiStatement { - return XapiStatement( - actor = actor, - verb = XapiVerb(id = VERB_VOIDED), - `object` = XapiStatementRef( - objectType = XapiObjectType.StatementRef, - id = statementId - ), - timestamp = Clock.System.now() - ) -} - - - diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 1ba617605..2d3a8e042 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -15,11 +15,12 @@ import world.respect.datalayer.SchoolDataSource import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.domain.xapi.createXapiAgentForQuery -import world.respect.shared.domain.xapi.toXapiAgent import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit import world.respect.shared.navigation.NavCommand @@ -30,9 +31,13 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.lib.xapi.model.XapiGroup.Companion.RESULT_KEY_GROUP_UPDATED import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS -import world.respect.shared.domain.xapi.createVoidingStatement +import world.respect.lib.xapi.model.XapiObjectType +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiStatementRef +import world.respect.lib.xapi.model.XapiVerb import world.respect.shared.viewmodel.app.appstate.FabUiState import kotlin.getValue +import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi @@ -117,12 +122,20 @@ class StudentGroupingDetailViewModel( val statementId = _uiState.value.statementGroupId ?: throw IllegalStateException("Statement ID not found when trying to delete group") - val person = respectAccountManager.selectedAccountAndPersonFlow.firstOrNull()?.person + val sessionAndPerson = respectAccountManager.selectedAccountAndPersonFlow.firstOrNull() ?: throw IllegalStateException("No person selected when trying to delete group") - val actor = person.toXapiAgent(schoolSelfUrl.toString()) + val actor = sessionAndPerson.xapiAgent - val voidingStatement = createVoidingStatement(actor, statementId) + val voidingStatement = XapiStatement( + actor = actor, + verb = XapiVerb(id = VERB_VOIDED), + `object` = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = statementId + ), + timestamp = Clock.System.now() + ) schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) _navCommandFlow.tryEmit(NavCommand.PopUp()) @@ -133,7 +146,13 @@ class StudentGroupingDetailViewModel( private fun loadGroupDetail() { viewModelScope.launch { // Query by agent (the group itself) using its account identifier - val groupAgent = createXapiAgentForQuery(route.groupId, schoolSelfUrl.toString()) + val groupAgent = XapiAgent( + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = route.groupId, + homePage = schoolSelfUrl.toString() + ) + ) schoolDataSource.xapiStatementsResource.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 43503ec62..a55567f19 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -23,6 +23,7 @@ import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.xapi.model.VERB_SAVED import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiAgent import world.respect.lib.xapi.model.XapiContext import world.respect.lib.xapi.model.XapiContextActivities import world.respect.lib.xapi.model.XapiGroup @@ -34,8 +35,9 @@ import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.domain.xapi.createVoidingStatement -import world.respect.shared.domain.xapi.toXapiAgent +import world.respect.datalayer.db.school.ext.fullName +import world.respect.lib.xapi.model.VERB_VOIDED +import world.respect.lib.xapi.model.XapiStatementRef import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit_group import world.respect.shared.generated.resources.create_group @@ -49,6 +51,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import kotlin.getValue +import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -190,19 +193,35 @@ class StudentGroupingEditViewModel( } launchWithLoadingIndicator { - val person = respectAccountManager.selectedAccountAndPersonFlow.first()?.person + val sessionAndPerson = respectAccountManager.selectedAccountAndPersonFlow.first() ?: throw IllegalStateException("No person selected when trying to save group") - val actor = person.toXapiAgent(schoolSelfUrl.toString()) + val actor = sessionAndPerson.xapiAgent if (existingStatementId != null) { - val voidingStatement = createVoidingStatement(actor, existingStatementId) + val voidingStatement = XapiStatement( + actor = actor, + verb = XapiVerb(id = VERB_VOIDED), + `object` = XapiStatementRef( + objectType = XapiObjectType.StatementRef, + id = existingStatementId + ), + timestamp = Clock.System.now() + ) schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) } // Create a new statement with the updated group information + val xapiHomePage = sessionAndPerson.session.account.school.xapi.toString() val members = _uiState.value.selectedStudents.map { student -> - student.toXapiAgent(schoolSelfUrl.toString()) + XapiAgent( + name = student.fullName(), + objectType = XapiObjectType.Agent, + account = XapiAccount( + name = student.guid, + homePage = xapiHomePage + ) + ) } val groupId = route.groupId ?: Uuid.random().toString() @@ -225,7 +244,7 @@ class StudentGroupingEditViewModel( actor = actor, verb = verb, `object` = group, - timestamp = kotlin.time.Clock.System.now(), + timestamp = Clock.System.now(), context = XapiContext( contextActivities = XapiContextActivities( parent = listOf( From b3324ae44944ea82456668a43d5eca3c858bd1b2 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 20 May 2026 17:28:51 +0530 Subject: [PATCH 073/130] Remove unnecessary empty lines in `ClazzDetailViewModel.kt`. --- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 5e3a0045b..91f75432d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -453,13 +453,11 @@ class ClazzDetailViewModel( ?: Instant.DISTANT_PAST } .map { statement -> - checkNotNull(statement.`object` as? XapiGroup) { "Expected XapiGroup object in statement ${statement.id}" } } .distinctBy { group -> - checkNotNull(group.account?.name) { "Group account name missing" } From b8a2ca238c859dfde5d3b59a90a47617b74eeb03 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 20 May 2026 17:43:55 +0530 Subject: [PATCH 074/130] Apply minor formatting and whitespace adjustments to `ClazzDetailViewModel.kt`. --- .../viewmodel/clazz/detail/ClazzDetailViewModel.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 91f75432d..226de59d4 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -75,10 +75,10 @@ import world.respect.lib.xapi.model.XapiGroup.Companion.CLASS import kotlin.time.Instant data class ClazzDetailUiState( - val teachers: IPagingSourceFactory = EmptyPagingSourceFactory(), + val teachers: IPagingSourceFactory = EmptyPagingSourceFactory() , val students: IPagingSourceFactory = EmptyPagingSourceFactory(), - val pendingTeachers: IPagingSourceFactory = EmptyPagingSourceFactory(), - val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory(), + val pendingTeachers:IPagingSourceFactory = EmptyPagingSourceFactory() , + val pendingStudents: IPagingSourceFactory = EmptyPagingSourceFactory() , val listOfPending: List = emptyList(), val chipOptions: List = emptyList(), @@ -109,7 +109,6 @@ data class ClazzDetailUiState( } - class ClazzDetailViewModel( savedStateHandle: SavedStateHandle, accountManager: RespectAccountManager, @@ -149,9 +148,9 @@ class ClazzDetailViewModel( } } - private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) + private val teacherPagingSource = pagingSourceByRole(EnrollmentRoleEnum.TEACHER) - private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) + private val studentPagingSource = pagingSourceByRole(EnrollmentRoleEnum.STUDENT) private val teachersPendingPagingSource = pagingSourceByRole(EnrollmentRoleEnum.PENDING_TEACHER) @@ -268,6 +267,7 @@ class ClazzDetailViewModel( } } } + } fun onClickAddPersonToClazz(roleType: EnrollmentRoleEnum) { @@ -292,7 +292,7 @@ class ClazzDetailViewModel( ) ) ) - } + } } fun onSortOrderChanged(sortOption: SortOrderOption) { From 129ac91e8fef8241b6904a66183ea6ab8067e7be Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 20 May 2026 21:35:46 +0530 Subject: [PATCH 075/130] Improve error handling and data robustness in student grouping view models and screens. - Add `Napier` logging and replace potential crashes with safe checks and early returns in `StudentGroupingDetailViewModel`, `ClazzDetailViewModel`, and `StudentGroupingEditViewModel`. - Refactor `StudentGroupingEditState` to calculate `selectedStudentIds` dynamically from the student list. - Enhance group filtering and mapping logic in `ClazzDetailViewModel` to handle missing group/member data gracefully. - Update UI components in `StudentGroupingEditScreen` and `ClazzDetailScreen` to safely handle null items and improve key mapping. - Delete unused `XapiActorDataSource.kt`. --- .../view/clazz/detail/ClazzDetailScreen.kt | 8 +- .../edit/StudentGroupingEditScreen.kt | 4 +- .../school/xapi/XapiActorDataSource.kt | 0 .../clazz/detail/ClazzDetailViewModel.kt | 28 +++++-- .../detail/StudentGroupingDetailViewModel.kt | 32 ++++++-- .../edit/StudentGroupingEditViewModel.kt | 81 +++++++++++-------- 6 files changed, 98 insertions(+), 55 deletions(-) delete mode 100644 respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 83e0abe29..dee7b0b6b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -435,14 +435,12 @@ fun ClazzDetailScreen( items( items = uiState.groups, - key = { group -> - requireNotNull(group.account?.name) - } + key = { group -> group.account?.name ?: "" } ) { group -> - val groupId = requireNotNull(group.account?.name) + val groupId = group.account?.name ?: return@items val memberNames = group.member - ?.mapNotNull { it.name } + ?.map { it.name.orEmpty() } ?: emptyList() ListItem( diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt index 959b15b8d..dd4884fce 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/studentgrouping/edit/StudentGroupingEditScreen.kt @@ -86,9 +86,7 @@ fun StudentGroupingEditScreen( } ) { student -> - val person = checkNotNull(student) { - "Student item should not be null" - } + val person = student ?: return@respectPagingItems val isSelected = person.guid in uiState.selectedStudentIds diff --git a/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt b/respect-datalayer/src/commonMain/kotlin/world/respect/datalayer/school/xapi/XapiActorDataSource.kt deleted file mode 100644 index e69de29bb..000000000 diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 226de59d4..278b2acf0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -130,7 +130,7 @@ class ClazzDetailViewModel( private val route: ClazzDetail = savedStateHandle.toRoute() - private val schoolSelfUrl: String = accountManager.activeAccount?.school?.self?.toString() + private val schoolSelfUrl = accountManager.activeAccount?.school?.self ?: throw IllegalStateException("schoolSelfUrl is required but activeAccount or school is null") private val classActivityId = "${schoolSelfUrl}${CLASS}${route.guid}" @@ -452,15 +452,31 @@ class ClazzDetailViewModel( ?: statement.stored ?: Instant.DISTANT_PAST } - .map { statement -> - checkNotNull(statement.`object` as? XapiGroup) { - "Expected XapiGroup object in statement ${statement.id}" + .mapNotNull { statement -> + val group = statement.`object` as? XapiGroup + if (group == null) { + Napier.w("observeGroupsFromXapi: Expected XapiGroup in statement ${statement.id}, skipping") } + group + } + .filter { group -> + val name = group.account?.name + if (name == null) { + Napier.w("observeGroupsFromXapi: Group account name missing, skipping") + } + name != null } .distinctBy { group -> - checkNotNull(group.account?.name) { - "Group account name missing" + group.account?.name + } + .map { group -> + val filteredMembers = group.member?.filter { agent -> + if (agent.name == null) { + Napier.w("observeGroupsFromXapi: member agent has no name in group ${group.account?.name}, skipping") + } + agent.name != null } + group.copy(member = filteredMembers) } _uiState.update { prev -> diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 2d3a8e042..b5dac573d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -3,6 +3,7 @@ package world.respect.shared.viewmodel.studentgrouping.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull @@ -94,7 +95,10 @@ class StudentGroupingDetailViewModel( fun onClickEdit() { val classId = _uiState.value.classId - ?: throw IllegalStateException("classId not loaded when trying to edit group") + if (classId == null) { + Napier.e("onClickEdit: classId not loaded when trying to edit group") + return + } _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingEdit( @@ -161,7 +165,11 @@ class StudentGroupingDetailViewModel( ), dataLoadParams = DataLoadParams() ).collect { dataLoadState -> - val statementResult = dataLoadState.dataOrNull() ?: return@collect + val statementResult = dataLoadState.dataOrNull() + if (statementResult == null) { + Napier.d("loadGroupDetail: data not yet loaded, waiting") + return@collect + } // Find the latest statement for this group by sorting by timestamp val groupStatement = statementResult.statements @@ -173,12 +181,22 @@ class StudentGroupingDetailViewModel( if (groupStatement != null) { val group = groupStatement.`object` as XapiGroup - val memberNames = group.member?.mapNotNull { it.name } ?: emptyList() - val statementId = checkNotNull(groupStatement.id) { - "Group statement id should not be null" + val memberNames = group.member?.mapNotNull { agent -> + val name = agent.name + if (name == null) { + Napier.w("loadGroupDetail: member agent has no name, skipping") + } + name + } ?: emptyList() + val statementId = groupStatement.id + if (statementId == null) { + Napier.e("loadGroupDetail: Group statement id is null, skipping update") + return@collect } - val groupName = checkNotNull(group.name) { - "Group name should not be null" + val groupName = group.name + if (groupName == null) { + Napier.e("loadGroupDetail: Group name is null, skipping update") + return@collect } val classActivityId = groupStatement.context?.contextActivities?.parent diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index a55567f19..ac979abf9 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -5,13 +5,14 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource +import io.github.aakira.napier.Napier import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.EnrollmentRoleEnum @@ -61,9 +62,11 @@ data class StudentGroupingEditUiState( val groupNameError: UiText? = null, val students: IPagingSourceFactory = EmptyPagingSourceFactory(), val selectedStudents: List = emptyList(), - val selectedStudentIds: Set = emptySet(), val statementId: String? = null -) +) { + val selectedStudentIds: Set + get() = selectedStudents.map { it.guid }.toSet() +} class StudentGroupingEditViewModel( savedStateHandle: SavedStateHandle, @@ -125,29 +128,30 @@ class StudentGroupingEditViewModel( route.groupId?.let { groupId -> @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { - val statementResult = schoolDataSource.xapiStatementsResource.get( - listParams = XapiStatementsResource.GetStatementParams( - verb = VERB_SAVED, - activity = classActivityId, - relatedActivities = true, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull() - ?: throw IllegalStateException( - "Failed to load statements for classActivityId=$classActivityId" - ) + try { + val statementResult = schoolDataSource.xapiStatementsResource.get( + listParams = XapiStatementsResource.GetStatementParams( + verb = VERB_SAVED, + activity = classActivityId, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull() + ?: throw IllegalStateException( + "Failed to load statements for classActivityId=$classActivityId" + ) - val groupStatement = statementResult.statements - .filter { statement -> - val group = statement.`object` as? XapiGroup - group?.account?.name == groupId - } - .maxByOrNull { - it.timestamp ?: it.stored ?: Instant.DISTANT_PAST - } - ?: throw IllegalStateException( - "Could not find statement for groupId=$groupId" - ) + val groupStatement = statementResult.statements + .filter { statement -> + val group = statement.`object` as? XapiGroup + group?.account?.name == groupId + } + .maxByOrNull { + it.timestamp ?: it.stored ?: Instant.DISTANT_PAST + } + ?: throw IllegalStateException( + "Could not find statement for groupId=$groupId" + ) val group = groupStatement.`object` as XapiGroup @@ -155,24 +159,36 @@ class StudentGroupingEditViewModel( val statementId = groupStatement.id val memberIds = group.member - ?.mapNotNull { it.account?.name } + ?.mapNotNull { agent -> + val name = agent.account?.name + if (name == null) { + Napier.w("StudentGroupingEditViewModel: member agent has no account name, skipping: $agent") + } + name + } ?: emptyList() val persons = memberIds.mapNotNull { id -> - schoolDataSource.personDataSource.findByGuid( + val person = schoolDataSource.personDataSource.findByGuid( DataLoadParams(), id ).dataOrNull() + if (person == null) { + Napier.w("StudentGroupingEditViewModel: could not find person with guid=$id") + } + person } _uiState.update { prev -> prev.copy( groupName = groupName ?: "", selectedStudents = persons, - selectedStudentIds = persons.map { it.guid }.toSet(), statementId = statementId.toString() ) } + } catch (e: Exception) { + Napier.e("StudentGroupingEditViewModel: error loading group", throwable = e) + } } } } @@ -193,7 +209,7 @@ class StudentGroupingEditViewModel( } launchWithLoadingIndicator { - val sessionAndPerson = respectAccountManager.selectedAccountAndPersonFlow.first() + val sessionAndPerson = respectAccountManager.selectedAccountAndPersonFlow.firstOrNull() ?: throw IllegalStateException("No person selected when trying to save group") val actor = sessionAndPerson.xapiAgent @@ -211,15 +227,13 @@ class StudentGroupingEditViewModel( schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) } - // Create a new statement with the updated group information - val xapiHomePage = sessionAndPerson.session.account.school.xapi.toString() val members = _uiState.value.selectedStudents.map { student -> XapiAgent( name = student.fullName(), objectType = XapiObjectType.Agent, account = XapiAccount( name = student.guid, - homePage = xapiHomePage + homePage = schoolSelfUrl.toString() ) ) } @@ -284,8 +298,7 @@ class StudentGroupingEditViewModel( } prev.copy( - selectedStudents = updated, - selectedStudentIds = updated.map { it.guid }.toSet() + selectedStudents = updated ) } } From 2685c5be3f9de403ae71d0aa99fe89cce2fb61ec Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 21 May 2026 11:02:21 +0530 Subject: [PATCH 076/130] Replaced showAddStudent, showAddTeacher, and showStudentGrouping with a single isAdminOrTeacher because all three were always set to the exact same value. --- .../app/view/clazz/detail/ClazzDetailScreen.kt | 16 ++++++++-------- .../clazz/detail/ClazzDetailViewModel.kt | 8 ++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index dee7b0b6b..98e5a2fc0 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -156,7 +156,7 @@ fun ClazzDetailScreen( ) } - if ((uiState.showAddTeacher || uiState.showAddStudent) && + if (uiState.isAdminOrTeacher && (pendingTeacherLazyPagingItems.itemCount + pendingStudentLazyPagingItems.itemCount) > 0 ) { item("pending_header") { @@ -196,7 +196,7 @@ fun ClazzDetailScreen( } if (uiState.isPendingExpanded) { - if (uiState.showAddTeacher) { + if (uiState.isAdminOrTeacher) { respectPagingItems( items = pendingTeacherLazyPagingItems, key = { person, index -> @@ -215,7 +215,7 @@ fun ClazzDetailScreen( } } - if (uiState.showAddStudent) { + if (uiState.isAdminOrTeacher) { respectPagingItems( items = pendingStudentLazyPagingItems, key = { person, index -> @@ -268,7 +268,7 @@ fun ClazzDetailScreen( } if (uiState.isTeachersExpanded) { - if (uiState.showAddTeacher) { + if (uiState.isAdminOrTeacher) { item("add_teacher") { ListItem( modifier = Modifier.clickable { @@ -297,7 +297,7 @@ fun ClazzDetailScreen( ) { teacher -> PersonListItemWithMenu( person = teacher, - showMenu = uiState.showAddTeacher, + showMenu = uiState.isAdminOrTeacher, onClickRemove = { onClickRemovePersonFromClass(it, EnrollmentRoleEnum.TEACHER) }, onClickManage = { onClickManageEnrollments(it, EnrollmentRoleEnum.TEACHER) }, onClick = onClickPerson, @@ -338,7 +338,7 @@ fun ClazzDetailScreen( } if (uiState.isStudentsExpanded) { - if(uiState.showAddStudent) { + if(uiState.isAdminOrTeacher) { item("add_student") { ListItem( modifier = Modifier.clickable { @@ -372,13 +372,13 @@ fun ClazzDetailScreen( onClickRemovePersonFromClass(it, EnrollmentRoleEnum.STUDENT) }, onClickManage = { onClickManageEnrollments(it, EnrollmentRoleEnum.STUDENT) }, - showMenu = uiState.showAddStudent, + showMenu = uiState.isAdminOrTeacher, onClick = onClickPerson, ) } } - if (uiState.showStudentGrouping) { + if (uiState.isAdminOrTeacher) { item("student_grouping_header") { ListItem( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index aac25c802..b0b0277ec 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -94,9 +94,7 @@ data class ClazzDetailUiState( val isTeachersExpanded: Boolean = true, val isStudentsExpanded: Boolean = true, val inviteCodePrefix: String? = null, - val showAddStudent: Boolean = false, - val showAddTeacher: Boolean = false, - val showStudentGrouping: Boolean = false, + val isAdminOrTeacher: Boolean = false, val isStudentGroupingExpanded: Boolean = true, val groups: List = emptyList(), val addPersonPermissions: List = emptyList() @@ -223,9 +221,7 @@ class ClazzDetailViewModel( accountManager.selectedAccountAndPersonFlow.collect { selectedAccountAndPerson -> _uiState.update { prev -> prev.copy( - showAddStudent = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - showAddTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true, - showStudentGrouping = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true + isAdminOrTeacher = selectedAccountAndPerson?.person?.isAdminOrTeacher() == true ) } From fc7d31376054faf66d52e86cd9941ebaf8799b28 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 21 May 2026 11:08:33 +0530 Subject: [PATCH 077/130] Refine log messages --- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 6 +++--- .../detail/StudentGroupingDetailViewModel.kt | 6 +++--- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index b0b0277ec..339c5347f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -456,14 +456,14 @@ class ClazzDetailViewModel( .mapNotNull { statement -> val group = statement.`object` as? XapiGroup if (group == null) { - Napier.w("observeGroupsFromXapi: Expected XapiGroup in statement ${statement.id}, skipping") + Napier.w("observeGroupsFromXapi: Expected XapiGroup in statement ${statement.id}") } group } .filter { group -> val name = group.account?.name if (name == null) { - Napier.w("observeGroupsFromXapi: Group account name missing, skipping") + Napier.w("observeGroupsFromXapi: Group account name missing") } name != null } @@ -473,7 +473,7 @@ class ClazzDetailViewModel( .map { group -> val filteredMembers = group.member?.filter { agent -> if (agent.name == null) { - Napier.w("observeGroupsFromXapi: member agent has no name in group ${group.account?.name}, skipping") + Napier.w("observeGroupsFromXapi: member agent has no name in group ${group.account?.name}") } agent.name != null } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index b5dac573d..2cf61adeb 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -184,18 +184,18 @@ class StudentGroupingDetailViewModel( val memberNames = group.member?.mapNotNull { agent -> val name = agent.name if (name == null) { - Napier.w("loadGroupDetail: member agent has no name, skipping") + Napier.w("loadGroupDetail: member agent has no name") } name } ?: emptyList() val statementId = groupStatement.id if (statementId == null) { - Napier.e("loadGroupDetail: Group statement id is null, skipping update") + Napier.e("loadGroupDetail: Group statement id is null") return@collect } val groupName = group.name if (groupName == null) { - Napier.e("loadGroupDetail: Group name is null, skipping update") + Napier.e("loadGroupDetail: Group name is null") return@collect } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index ac979abf9..f6168b293 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -162,7 +162,7 @@ class StudentGroupingEditViewModel( ?.mapNotNull { agent -> val name = agent.account?.name if (name == null) { - Napier.w("StudentGroupingEditViewModel: member agent has no account name, skipping: $agent") + Napier.w("StudentGroupingEditViewModel: member agent has no account name") } name } From 06f7f39736c67d3aa87ffe9e210389dd508cbe3f Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 21 May 2026 15:33:24 +0530 Subject: [PATCH 078/130] Refine log messages --- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index f6168b293..0dfb3d4fc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -157,6 +157,10 @@ class StudentGroupingEditViewModel( val groupName = group.name val statementId = groupStatement.id + if (statementId == null) { + Napier.e("StudentGroupingEditViewModel: group statement id is null for groupId=$groupId, cannot edit") + return@launch + } val memberIds = group.member ?.mapNotNull { agent -> From 9bb25c66bce994472944c10392a4b7b77f33caad Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 21 May 2026 17:02:44 +0530 Subject: [PATCH 079/130] Add `STACK_COUNT` constant to `ClazzDetailViewModel` and use it to limit member display in `ClazzDetailScreen`. --- .../world/respect/app/view/clazz/detail/ClazzDetailScreen.kt | 3 ++- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 98e5a2fc0..a7398ba02 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -65,6 +65,7 @@ import world.respect.shared.viewmodel.clazz.detail.ClazzDetailUiState import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel import world.respect.shared.generated.resources.create_group import world.respect.shared.generated.resources.groups +import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.STACK_COUNT @Composable fun ClazzDetailScreen( @@ -453,7 +454,7 @@ fun ClazzDetailScreen( Box( modifier = Modifier.width(40.dp), ) { - val displayMembers = memberNames.take(3) + val displayMembers = memberNames.take(STACK_COUNT) displayMembers.forEachIndexed { i, name -> Box( modifier = Modifier diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 339c5347f..490e40e6f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -491,5 +491,7 @@ class ClazzDetailViewModel( const val RESULT_KEY_PREFIX = "result_" + const val STACK_COUNT=3 + } } From c7398bc176c73e9fd2ede0b90535893c414af975 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 3 Jun 2026 14:16:53 +0530 Subject: [PATCH 080/130] Update `004_001_add_groups_to_a_class_test.yaml` to pass a class name environment variable to the `admin_add_class_with_members` subflow. --- .maestro/flows/004_001_add_groups_to_a_class_test.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index c0df83505..1e531e13b 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -17,7 +17,10 @@ onFlowComplete: file: "scripts/teardown.js" --- - runFlow: "subflows/school_admin_login_flow.yaml" -- runFlow: "subflows/admin_add_class_with_members.yaml" +- runFlow: + file: "subflows/admin_add_class_with_members.yaml" + env: + CLASSNAME: "TestClass" # - clearState: world.respect.app - launchApp: From 1be2c51e972a0ccde363a7528f1faf00c213e1dd Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 4 Jun 2026 00:52:41 +0530 Subject: [PATCH 081/130] Update teacher password in Maestro flow `004_001_add_groups_to_a_class_test.yaml`. --- .maestro/flows/004_001_add_groups_to_a_class_test.yaml | 2 +- .maestro/flows/subflows/admin_add_class_with_members.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index 1e531e13b..24d902b3b 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -36,7 +36,7 @@ onFlowComplete: - inputText: "teacherauser" - tapOn: id : "password" -- inputText: "test1234" +- inputText: "test123" - tapOn: "Login" - runFlow: when: diff --git a/.maestro/flows/subflows/admin_add_class_with_members.yaml b/.maestro/flows/subflows/admin_add_class_with_members.yaml index 64e426fc9..2d0e05c9b 100644 --- a/.maestro/flows/subflows/admin_add_class_with_members.yaml +++ b/.maestro/flows/subflows/admin_add_class_with_members.yaml @@ -34,6 +34,12 @@ appId: world.respect.app CLASSNAME: "TestClass" FIRSTNAME: StudentC LASTNAME: User +- runFlow: + file: "admin_add_new_student_to_class.yaml" + env: + CLASSNAME: "TestClass" + FIRSTNAME: StudentD + LASTNAME: User - runFlow: file: "admin_add_new_teacher_to_class.yaml" From 9d652accb319204515224efa41771068bdd27fd7 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 4 Jun 2026 13:05:42 +0530 Subject: [PATCH 082/130] Update Maestro test flows to use "StudentB USER" instead of "StudentD USER" and remove "StudentD" from the class member setup. --- .maestro/flows/004_001_add_groups_to_a_class_test.yaml | 4 ++-- .maestro/flows/subflows/admin_add_class_with_members.yaml | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml index 24d902b3b..5353becc2 100644 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml @@ -81,12 +81,12 @@ onFlowComplete: id: "delete_group_btn" - tapOn: id: "floating_action_button" -- tapOn: "StudentD USER" +- tapOn: "StudentB USER" - tapOn: "Save" - assertVisible: id: "app_title" text: "Test Group" -- assertVisible: "StudentD USER" +- assertVisible: "StudentB USER" - assertVisible: "3 Students" - back - assertVisible: diff --git a/.maestro/flows/subflows/admin_add_class_with_members.yaml b/.maestro/flows/subflows/admin_add_class_with_members.yaml index 2d0e05c9b..64e426fc9 100644 --- a/.maestro/flows/subflows/admin_add_class_with_members.yaml +++ b/.maestro/flows/subflows/admin_add_class_with_members.yaml @@ -34,12 +34,6 @@ appId: world.respect.app CLASSNAME: "TestClass" FIRSTNAME: StudentC LASTNAME: User -- runFlow: - file: "admin_add_new_student_to_class.yaml" - env: - CLASSNAME: "TestClass" - FIRSTNAME: StudentD - LASTNAME: User - runFlow: file: "admin_add_new_teacher_to_class.yaml" From cd8602d1dd00049ccaac6d515ef22ceac8601a4d Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 5 Jun 2026 11:57:51 +0530 Subject: [PATCH 083/130] Add `ID_TYPE_CLASS` and `ID_CLASS_MANAGEMENT` constants to `XapiVerb`. --- .../kotlin/world/respect/lib/xapi/model/XapiVerb.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index ce4bd51f1..bdafffa48 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -32,6 +32,11 @@ data class XapiVerb( */ const val ID_ASSIGN = "http://activitystrea.ms/schema/1.0/assign" + private const val ID_TYPE_CLASS = "http://id.openeel.org/xapi/activity-type/class" + + private const val ID_CLASS_MANAGEMENT = "https://id.openeel.org/xapi/recipes/class-management" + + } } From 7af89bc2c2ce3feece75afca2645ece4cd10fcfb Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 8 Jun 2026 23:08:24 +0530 Subject: [PATCH 084/130] Refactor class editing to use xAPI statements and recipes. - Add `XapiClassExt.kt` with extension properties and helper functions for handling class-management xAPI statements. - Update `ClazzEditViewModel` and `ClazzEditScreen` to use `XapiStatement` as the primary data model instead of the legacy `Clazz` model. - Implement xAPI-based persistence in `ClazzEditViewModel` using `XapiStatementsResource`. --- .../app/view/clazz/edit/ClazzEditScreen.kt | 27 +-- .../shared/domain/xapi/XapiClassExt.kt | 109 ++++++++++++ .../respect/shared/navigation/AppRoutes.kt | 4 +- .../clazz/edit/ClazzEditViewModel.kt | 165 +++++++++++------- 4 files changed, 225 insertions(+), 80 deletions(-) create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt index 724a11942..5e6cf881b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt @@ -17,7 +17,11 @@ import org.jetbrains.compose.resources.stringResource import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource import world.respect.lib.dataloadstate.ext.dataOrNull -import world.respect.datalayer.school.model.Clazz +import world.respect.lib.xapi.model.XapiStatement +import world.respect.shared.domain.xapi.classDefinitionTitle +import world.respect.shared.domain.xapi.classDefinitionDescription +import world.respect.shared.domain.xapi.withClassTitle +import world.respect.shared.domain.xapi.withClassDescription import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.class_name import world.respect.shared.generated.resources.description @@ -34,18 +38,18 @@ fun ClazzEditScreen( ClazzEditScreen( uiState = uiState, onEntityChanged = viewModel::onEntityChanged, - onClearError = viewModel::onClearError + onClearError = viewModel::onClearError ) } @Composable fun ClazzEditScreen( uiState: ClazzEditUiState, - onEntityChanged: (Clazz) -> Unit = {}, + onEntityChanged: (XapiStatement) -> Unit = {}, onClearError: () -> Unit = {}, ) { - val clazz = uiState.clazz.dataOrNull() + val statement = uiState.statementData.dataOrNull() val fieldsEnabled = uiState.fieldsEnabled Column( @@ -54,13 +58,13 @@ fun ClazzEditScreen( ) { OutlinedTextField( modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag("name"), - value = clazz?.title ?: "", + value = statement?.classDefinitionTitle ?: "", label = { Text(stringResource(Res.string.class_name) + "*") }, onValueChange = { value -> - clazz?.also { - onEntityChanged(it.copy(title = value)) + statement?.also { + onEntityChanged(it.withClassTitle(value)) } if (uiState.clazzNameError != null && value.isNotBlank()) { onClearError() @@ -76,17 +80,18 @@ fun ClazzEditScreen( OutlinedTextField( modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag(("description")), - value = clazz?.description ?: "", + value = statement?.classDefinitionDescription ?: "", label = { Text( stringResource(Res.string.description) ) }, onValueChange = { newValue -> - clazz?.also { - onEntityChanged(it.copy(description = newValue)) + statement?.also { + onEntityChanged(it.withClassDescription(newValue)) } - } + }, + enabled = fieldsEnabled ) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt new file mode 100644 index 000000000..72bba15a3 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt @@ -0,0 +1,109 @@ +package world.respect.shared.domain.xapi + +import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiActivityDefinition +import world.respect.lib.xapi.model.XapiActor +import world.respect.lib.xapi.model.XapiContext +import world.respect.lib.xapi.model.XapiContextActivities +import world.respect.lib.xapi.model.XapiObjectType +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiVerb +import kotlin.time.Clock +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** + * Activity type for a class as per README_CLASS_RECIPE.md + */ +const val ACTIVITY_TYPE_CLASS = "http://id.openeel.org/xapi/activity-type/class" + +/** + * Category activity id for the class-management recipe as per README_CLASS_RECIPE.md + */ +const val CATEGORY_CLASS_MANAGEMENT = "https://id.openeel.org/xapi/recipes/class-management" + +const val ACTIVITY_ID_PATH = "xapi/activities/classes" + + +/** + * The activity definition name for a class statement. + */ +@OptIn(ExperimentalUuidApi::class) +val XapiStatement.classDefinitionTitle: String + get() = (this.`object` as? XapiActivity)?.definition?.name?.values?.firstOrNull() ?: "" + +/** + * The activity definition description for a class statement. + */ +val XapiStatement.classDefinitionDescription: String + get() = (this.`object` as? XapiActivity)?.definition?.description?.values?.firstOrNull() ?: "" + +/** + * Create a copy of this class statement with an updated title in the activity definition name. + */ +@OptIn(ExperimentalUuidApi::class) +fun XapiStatement.withClassTitle(title: String): XapiStatement { + val activity = `object` as? XapiActivity ?: return this + val definition = activity.definition ?: XapiActivityDefinition() + return copy( + `object` = activity.copy( + definition = definition.copy( + name = (definition.name ?: emptyMap()) + ("" to title) + ) + ) + ) +} + +/** + * Create a copy of this class statement with an updated description in the activity definition. + */ +fun XapiStatement.withClassDescription(description: String): XapiStatement { + val activity = `object` as? XapiActivity ?: return this + val definition = activity.definition ?: XapiActivityDefinition() + return copy( + `object` = activity.copy( + definition = definition.copy( + description = (definition.description ?: emptyMap()) + ("" to description) + ) + ) + ) +} + +/** + * Create a blank class statement as per the class-management recipe. + */ +@OptIn(ExperimentalUuidApi::class) +fun createBlankClassStatement( + classActivityId: String, + actor: XapiActor +): XapiStatement { + val now = Clock.System.now() + return XapiStatement( + id = Uuid.random(), + actor = actor, + verb = XapiVerb( + id = XapiVerb.ID_SAVED, + ), + `object` = XapiActivity( + objectType = XapiObjectType.Activity, + id = classActivityId, + definition = XapiActivityDefinition( + name = emptyMap(), + description = emptyMap(), + type = ACTIVITY_TYPE_CLASS, + ) + ), + context = XapiContext( + contextActivities = XapiContextActivities( + category = listOf( + XapiActivity( + id = CATEGORY_CLASS_MANAGEMENT, + objectType = XapiObjectType.Activity + ) + ), + ), + ), + timestamp = now, + version = "1.0.0" + ) +} diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 4ea735117..8fb88c3f6 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -156,7 +156,7 @@ object ClazzList : RespectAppRoute @Serializable class ClazzDetail( - val guid: String, + val classActivityId: String, ) : RespectAppRoute @Serializable @@ -216,7 +216,7 @@ class AddPersonToClazz( @Serializable data class ClazzEdit( - val guid: String? + val classActivityId: String?, ) : RespectAppRoute @Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index fff5e8a97..444e348b4 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.serialization.json.Json @@ -17,12 +18,16 @@ import world.respect.lib.dataloadstate.DataReadyState import world.respect.datalayer.SchoolDataSource import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.dataloadstate.ext.isReadyAndSettled -import world.respect.datalayer.school.model.Clazz -import world.respect.datalayer.school.model.Clazz.Companion.DEFAULT_INVITE_CODE_LEN -import world.respect.datalayer.school.model.Clazz.Companion.DEFAULT_INVITE_CODE_MAX +import world.respect.lib.dataloadstate.ext.firstOrNotLoaded +import world.respect.lib.dataloadstate.ext.map +import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams +import world.respect.libutil.ext.appendEndpointSegments import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.domain.createclass.CreateClassUseCase -import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator +import world.respect.shared.domain.xapi.ACTIVITY_ID_PATH +import world.respect.shared.domain.xapi.classDefinitionTitle +import world.respect.shared.domain.xapi.createBlankClassStatement import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_clazz import world.respect.shared.generated.resources.edit_clazz @@ -36,20 +41,26 @@ import world.respect.shared.util.LaunchDebouncer import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState -import kotlin.random.Random import kotlin.time.Clock +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + data class ClazzEditUiState( + val statementData: DataLoadState = DataLoadingState(), val clazzNameError: UiText? = null, - val clazz: DataLoadState = DataLoadingState(), ) { val fieldsEnabled: Boolean - get() = clazz.isReadyAndSettled() + get() = statementData.isReadyAndSettled() + + val hasErrors: Boolean + get() = clazzNameError != null } +@OptIn(ExperimentalUuidApi::class) class ClazzEditViewModel( savedStateHandle: SavedStateHandle, - accountManager: RespectAccountManager, + private val accountManager: RespectAccountManager, private val json: Json, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { @@ -58,24 +69,25 @@ class ClazzEditViewModel( private val schoolDataSource: SchoolDataSource by inject() private val route: ClazzEdit = savedStateHandle.toRoute() - private val schoolPrimaryKeyGenerator: SchoolPrimaryKeyGenerator by inject() - - private val createClassUseCase: CreateClassUseCase by inject() - - private val guid = route.guid ?: schoolPrimaryKeyGenerator.primaryKeyGenerator.nextId( - Clazz.TABLE_ID - ).toString() - private val _uiState = MutableStateFlow(ClazzEditUiState()) val uiState = _uiState.asStateFlow() + private val schoolUrl = accountManager.requireActiveSchoolUrl() + + private val classActivityId = route.classActivityId ?: run { + schoolUrl.appendEndpointSegments( + ACTIVITY_ID_PATH, + Uuid.random().toString() + ).toString() + } + private val debouncer = LaunchDebouncer(viewModelScope) init { _appUiState.update { prev -> prev.copy( - title = if (route.guid == null) { + title = if (route.classActivityId == null) { Res.string.add_clazz.asUiText() } else { Res.string.edit_clazz.asUiText() @@ -91,87 +103,106 @@ class ClazzEditViewModel( } launchWithLoadingIndicator { - if (route.guid != null) { + if (route.classActivityId != null) { loadEntity( json = json, - serializer = Clazz.serializer(), + serializer = XapiStatement.serializer(), loadFn = { params -> - schoolDataSource.classDataSource.findByGuid(params, guid) + schoolDataSource.xapiStatementsResource.get( + listParams = GetStatementParams( + activity = classActivityId, + ), + dataLoadParams = params + ).map { result -> + result.statements.mostRecentByTimestampOrNull()?.let { + listOf(it) + } ?: emptyList() + }.firstOrNotLoaded() }, - uiUpdateFn = { clazz -> - _uiState.update { prev -> prev.copy(clazz = clazz) } + uiUpdateFn = { entity -> + _uiState.update { prev -> + prev.copy( + statementData = entity + ) + } } ) } else { + val actor = accountManager.selectedAccountAndPersonFlow.firstOrNull()?.xapiAgent + ?: return@launchWithLoadingIndicator + val baseStmt = createBlankClassStatement( + classActivityId = classActivityId, + actor = actor + ) + _uiState.update { prev -> prev.copy( - clazz = DataReadyState( - Clazz( - guid = guid, - title = "", - description = "", - ) - ) + statementData = DataReadyState(baseStmt) ) } } } } - fun onEntityChanged(clazz: Clazz) { - val classToCommit = _uiState.updateAndGet { prev -> - prev.copy(clazz = DataReadyState(clazz)) - }.clazz.dataOrNull() ?: return + fun onEntityChanged(statement: XapiStatement) { + _uiState.update { prev -> + prev.copy( + statementData = DataReadyState(statement), + clazzNameError = prev.clazzNameError?.takeIf { + prev.statementData.dataOrNull()?.classDefinitionTitle == statement.classDefinitionTitle + }, + ) + } debouncer.launch(DEFAULT_SAVED_STATE_KEY) { - savedStateHandle[DEFAULT_SAVED_STATE_KEY] = json.encodeToString(classToCommit) + savedStateHandle[DEFAULT_SAVED_STATE_KEY] = + json.encodeToString(XapiStatement.serializer(), statement) } } fun onClickSave() { - val clazz = _uiState.value.clazz.dataOrNull()?.copy( - lastModified = Clock.System.now() - ) ?: return + val stateToSave = _uiState.updateAndGet { prev -> + val statement = prev.statementData.dataOrNull() - if (clazz.title.isBlank()) { - _uiState.update { prev -> - prev.copy(clazzNameError = Res.string.required_field.asUiText()) - } - return - } else { - _uiState.update { prev -> prev.copy(clazzNameError = null) } + prev.copy( + clazzNameError = Res.string.required_field.asUiText().takeIf { + statement?.classDefinitionTitle.isNullOrBlank() + }, + ) } - launchWithLoadingIndicator { - try { - if (route.guid == null) { - val newClazz = clazz.copy() + if (stateToSave.hasErrors) + return - createClassUseCase(newClazz) + val classStatement = _uiState.value.statementData.dataOrNull() ?: return - _navCommandFlow.tryEmit( - NavCommand.Navigate( - ClazzDetail(guid), popUpTo = route, popUpToInclusive = true - ) + launchWithLoadingIndicator { + schoolDataSource.xapiStatementsResource.post( + listOf( + classStatement.copy( + id = Uuid.random(), + timestamp = Clock.System.now(), ) - } else { - schoolDataSource.classDataSource.store(listOf(clazz)) - _navCommandFlow.tryEmit(NavCommand.PopUp()) - } - } catch (e: Throwable) { - //needs to display snack bar here - e.printStackTrace() + ) + ) + + if (route.classActivityId == null) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = ClazzDetail( + classActivityId = classActivityId, + ), + popUpTo = route, + popUpToInclusive = true, + ) + ) + } else { + _navCommandFlow.tryEmit(NavCommand.PopUp()) } } } - private fun generateCode(): String { - return Random.nextInt(DEFAULT_INVITE_CODE_MAX) - .toString() - .padStart(DEFAULT_INVITE_CODE_LEN, '0') - } fun onClearError() { _uiState.update { prev -> prev.copy(clazzNameError = null) } } - } \ No newline at end of file From 6568c1c3953ecd535540f2c734f978912d94e4bf Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 8 Jun 2026 23:08:35 +0530 Subject: [PATCH 085/130] Refactor class editing to use xAPI statements and recipes. - Add `XapiClassExt.kt` with extension properties and helper functions for handling class-management xAPI statements. - Update `ClazzEditViewModel` and `ClazzEditScreen` to use `XapiStatement` as the primary data model instead of the legacy `Clazz` model. - Implement xAPI-based persistence in `ClazzEditViewModel` using `XapiStatementsResource`. --- .../commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index bdafffa48..9d204529d 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -32,9 +32,6 @@ data class XapiVerb( */ const val ID_ASSIGN = "http://activitystrea.ms/schema/1.0/assign" - private const val ID_TYPE_CLASS = "http://id.openeel.org/xapi/activity-type/class" - - private const val ID_CLASS_MANAGEMENT = "https://id.openeel.org/xapi/recipes/class-management" } From d55c5b8d1015d8106b07b5fc8e8a1ac2221b4f64 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 8 Jun 2026 23:20:56 +0530 Subject: [PATCH 086/130] Refactor class editing to use xAPI statements and recipes. - Add `XapiClassExt.kt` with extension properties and helper functions for handling class-management xAPI statements. - Update `ClazzEditViewModel` and `ClazzEditScreen` to use `XapiStatement` as the primary data model instead of the legacy `Clazz` model. - Implement xAPI-based persistence in `ClazzEditViewModel` using `XapiStatementsResource`. --- .../shared/domain/xapi/XapiClassExt.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt index 72bba15a3..5e36d1660 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt @@ -12,6 +12,8 @@ import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +const val XAPI_LANG_KEY = "" + /** * Activity type for a class as per README_CLASS_RECIPE.md */ @@ -38,36 +40,34 @@ val XapiStatement.classDefinitionTitle: String val XapiStatement.classDefinitionDescription: String get() = (this.`object` as? XapiActivity)?.definition?.description?.values?.firstOrNull() ?: "" -/** - * Create a copy of this class statement with an updated title in the activity definition name. - */ -@OptIn(ExperimentalUuidApi::class) -fun XapiStatement.withClassTitle(title: String): XapiStatement { - val activity = `object` as? XapiActivity ?: return this + +private fun XapiStatement.withDefinitionField( + update: (XapiActivityDefinition) -> XapiActivityDefinition +): XapiStatement { + val activity = `object` as? XapiActivity + ?: throw IllegalStateException("Statement object is not an XapiActivity") val definition = activity.definition ?: XapiActivityDefinition() return copy( - `object` = activity.copy( - definition = definition.copy( - name = (definition.name ?: emptyMap()) + ("" to title) - ) - ) + `object` = activity.copy(definition = update(definition)) ) } +/** + * Create a copy of this class statement with an updated title in the activity definition name. + */ +fun XapiStatement.withClassTitle(title: String): XapiStatement = + withDefinitionField { def -> + def.copy(name = (def.name ?: emptyMap()) + (XAPI_LANG_KEY to title)) + } + /** * Create a copy of this class statement with an updated description in the activity definition. */ -fun XapiStatement.withClassDescription(description: String): XapiStatement { - val activity = `object` as? XapiActivity ?: return this - val definition = activity.definition ?: XapiActivityDefinition() - return copy( - `object` = activity.copy( - definition = definition.copy( - description = (definition.description ?: emptyMap()) + ("" to description) - ) - ) - ) -} +fun XapiStatement.withClassDescription(description: String): XapiStatement = + withDefinitionField { def -> + def.copy(description = (def.description ?: emptyMap()) + (XAPI_LANG_KEY to description)) + } + /** * Create a blank class statement as per the class-management recipe. From a2e9e1c8aa1e325dfc4043ab09eb6c21ec265dac Mon Sep 17 00:00:00 2001 From: mandviverma Date: Mon, 8 Jun 2026 23:26:25 +0530 Subject: [PATCH 087/130] Remove redundant blank lines in `XapiVerb.kt`. --- .../commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt index 9d204529d..ce4bd51f1 100644 --- a/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt +++ b/respect-lib-xapi-core/src/commonMain/kotlin/world/respect/lib/xapi/model/XapiVerb.kt @@ -32,8 +32,6 @@ data class XapiVerb( */ const val ID_ASSIGN = "http://activitystrea.ms/schema/1.0/assign" - - } } From ce379cd395dd0135fb5832fe4908d4aca0eb15d3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 9 Jun 2026 15:58:10 +0530 Subject: [PATCH 088/130] Remove redundant blank lines in `XapiVerb.kt`. --- .../kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt index 5e36d1660..233686196 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt @@ -12,7 +12,7 @@ import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -const val XAPI_LANG_KEY = "" +const val XAPI_LANG_KEY = "en-US" /** * Activity type for a class as per README_CLASS_RECIPE.md From 7409512e7519a633fc00f0a48732d4071f0dff5b Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 03:48:24 +0530 Subject: [PATCH 089/130] Migrate class list and detail views to use xAPI statements instead of legacy `Clazz` data models. - Replace `Clazz` model usage with `XapiStatement` in `ClazzDetailViewModel` and `ClazzListViewModel`. - Update `ClazzDetailViewModel` to fetch class data via `schoolDataSource.xapiStatementsResource` using the activity ID from navigation. - Refactor `ClazzListViewModel` to filter and display the most recent class-related xAPI statements instead of using a paging source. - Update `ClazzDetailScreen` and `ClazzListScreen` to bind data from xAPI statement definitions (title and description). - Update navigation routes and related use cases to utilize activity IDs. --- .../view/clazz/detail/ClazzDetailScreen.kt | 5 +- .../app/view/clazz/list/ClazzListScreen.kt | 35 ++++++------ .../clazz/detail/ClazzDetailViewModel.kt | 51 ++++++++++++----- .../clazz/list/ClazzListViewModel.kt | 56 +++++++++++++------ 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 326a98e2f..d697b2b3b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -35,6 +35,7 @@ import world.respect.datalayer.db.school.ext.fullName import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person +import world.respect.shared.domain.xapi.classDefinitionDescription import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_student import world.respect.shared.generated.resources.add_teacher @@ -43,13 +44,11 @@ import world.respect.shared.generated.resources.pending_requests import world.respect.shared.generated.resources.collapse_pending_invites import world.respect.shared.generated.resources.collapse_students import world.respect.shared.generated.resources.collapse_teachers -import world.respect.shared.generated.resources.description import world.respect.shared.generated.resources.expand_pending_invites import world.respect.shared.generated.resources.expand_students import world.respect.shared.generated.resources.expand_teachers import world.respect.shared.generated.resources.manage_enrollments import world.respect.shared.generated.resources.more_options -import world.respect.shared.generated.resources.pending_requests import world.respect.shared.generated.resources.remove_from_class import world.respect.shared.generated.resources.student import world.respect.shared.generated.resources.students @@ -130,7 +129,7 @@ fun ClazzDetailScreen( }, supportingContent = { - Text(text = uiState.clazz.dataOrNull()?.description ?: "") + Text(text = uiState.classStatement.dataOrNull()?.classDefinitionDescription ?: "") } ) } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt index 4572dfc92..a000af04e 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt @@ -4,23 +4,22 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.paging.compose.collectAsLazyPagingItems import world.respect.app.components.RespectEmptyListComponent import world.respect.app.components.RespectListSortHeader import world.respect.app.components.RespectPersonAvatar import world.respect.app.components.SortListMode import world.respect.app.components.defaultItemPadding import world.respect.app.components.defaultSortListMode -import world.respect.app.components.respectPagingItems -import world.respect.app.components.respectRememberPager -import world.respect.datalayer.school.ClassDataSource -import world.respect.datalayer.school.model.Clazz +import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiStatement +import world.respect.shared.domain.xapi.classDefinitionTitle import world.respect.shared.util.SortOrderOption import world.respect.shared.viewmodel.clazz.list.ClazzListUiState import world.respect.shared.viewmodel.clazz.list.ClazzListViewModel @@ -42,15 +41,11 @@ fun ClazzListScreen( @Composable fun ClazzListScreen( uiState: ClazzListUiState, - onClickClazz: (Clazz) -> Unit, + onClickClazz: (XapiStatement) -> Unit, onClickSortOption: (SortOrderOption) -> Unit = { }, sortListMode: SortListMode = defaultSortListMode(), ) { - val pager = respectRememberPager(uiState.classes) - - val lazyPagingItems = pager.flow.collectAsLazyPagingItems() - LazyColumn(modifier = Modifier.fillMaxSize()) { item("header") { @@ -64,29 +59,31 @@ fun ClazzListScreen( ) } - respectPagingItems( - items = lazyPagingItems, - key = { item, index -> item?.guid ?: index.toString() }, - contentType = { ClassDataSource.ENDPOINT_NAME }, - ) { clazz -> + items( + items = uiState.classStatements, + key = { statement -> + (statement.`object` as? XapiActivity)?.id ?: statement.id.toString() + }, + ) { statement -> + val title = statement.classDefinitionTitle ListItem( modifier = Modifier .fillMaxWidth() .clickable { - clazz?.also(onClickClazz) + onClickClazz(statement) }, leadingContent = { - RespectPersonAvatar(name = clazz?.title ?: "") + RespectPersonAvatar(name = title) }, headlineContent = { - Text(text = clazz?.title ?: "") + Text(text = title) } ) } - if(lazyPagingItems.itemCount == 0) { + if(uiState.classStatements.isEmpty()) { item("empty_item") { RespectEmptyListComponent(Modifier.fillMaxWidth().defaultItemPadding()) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 6c6e59c3a..472596bb0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -16,11 +16,11 @@ import org.koin.core.scope.Scope import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.DataLoadState import world.respect.lib.dataloadstate.DataLoadingState +import world.respect.lib.dataloadstate.DataReadyState import world.respect.datalayer.SchoolDataSource import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.school.EnrollmentDataSource import world.respect.datalayer.school.PersonDataSource -import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.Enrollment import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person @@ -32,6 +32,8 @@ import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.account.invite.ApproveOrDeclineInviteRequestUseCase import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator +import world.respect.shared.domain.xapi.classDefinitionTitle +import world.respect.shared.domain.xapi.classDefinitionDescription import world.respect.shared.ext.whenSubscribed import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.first_name @@ -58,6 +60,10 @@ import world.respect.datalayer.school.ext.writePermissionFlag import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.writequeue.EnqueueRunPullSyncUseCase +import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull +import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams import world.respect.shared.domain.enrollments.UpdateClazzStudentXapiGroupUseCase import world.respect.shared.domain.permissions.CheckSchoolPermissionsUseCase import world.respect.shared.ext.tryOrShowSnackbarOnError @@ -83,7 +89,7 @@ data class ClazzDetailUiState( Res.string.first_name, 1, true ), val fieldsEnabled: Boolean = true, - val clazz: DataLoadState = DataLoadingState(), + val classStatement: DataLoadState = DataLoadingState(), val isPendingExpanded: Boolean = true, val isTeachersExpanded: Boolean = true, val isStudentsExpanded: Boolean = true, @@ -129,7 +135,7 @@ class ClazzDetailViewModel( schoolDataSource.personDataSource.listAsPagingSource( loadParams = DataLoadParams(), params = PersonDataSource.GetListParams( - filterByClazzUid = route.guid, + filterByClazzUid = route.classActivityId, filterByEnrolmentRole = role, inClassOnDay = localDateInCurrentTimeZone(), ) @@ -192,11 +198,25 @@ class ClazzDetailViewModel( } viewModelScope.launch { - schoolDataSource.classDataSource.findByGuidAsFlow(route.guid).collect { clazz -> + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = GetStatementParams( + activity = route.classActivityId, + ), + dataLoadParams = DataLoadParams(), + ).collect { dataLoadState -> + val statement = dataLoadState.dataOrNull()?.statements + ?.mostRecentByTimestampOrNull() + _appUiState.update { - it.copy(title = clazz.dataOrNull()?.title?.asUiText()) + it.copy(title = statement?.classDefinitionTitle?.asUiText()) + } + _uiState.update { + it.copy( + classStatement = statement?.let { stmt -> + DataReadyState(stmt) + } ?: DataLoadingState() + ) } - _uiState.update { it.copy(clazz = clazz) } } } @@ -236,7 +256,7 @@ class ClazzDetailViewModel( uid = schoolPrimaryKeyGenerator.primaryKeyGenerator.nextId( Enrollment.TABLE_ID ).toString(), - classUid = route.guid, + classUid = route.classActivityId, role = enrolmentRole, personUid = personToEnrol.guid, beginDate = Clock.System.now().toLocalDateTime( @@ -247,7 +267,7 @@ class ClazzDetailViewModel( ) if(enrolmentRole == EnrollmentRoleEnum.STUDENT) { - updateClazzStudentXapiGroupUseCase(route.guid) + updateClazzStudentXapiGroupUseCase(route.classActivityId) } } } @@ -258,7 +278,8 @@ class ClazzDetailViewModel( fun onClickAddPersonToClazz(roleType: EnrollmentRoleEnum) { viewModelScope.launch { - val clazz = _uiState.value.clazz.dataOrNull() ?: return@launch + val statement = _uiState.value.classStatement.dataOrNull() ?: return@launch + val classTitle = statement.classDefinitionTitle _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -269,10 +290,10 @@ class ClazzDetailViewModel( resultPopUpTo = route, ), inviteUid = ClassInvite.uidFor( - route.guid, roleType, ClassInviteModeEnum.DIRECT + route.classActivityId, roleType, ClassInviteModeEnum.DIRECT ), - classUid = clazz.guid, - className = clazz.title, + classUid = route.classActivityId, + className = classTitle, addToClassRole = roleType, filterByRole = roleType.relatedPersonRoleEnum, ) @@ -327,7 +348,7 @@ class ClazzDetailViewModel( fun onClickEdit() { _navCommandFlow.tryEmit( - NavCommand.Navigate(ClazzEdit(route.guid)) + NavCommand.Navigate(ClazzEdit(route.classActivityId)) ) } @@ -338,7 +359,7 @@ class ClazzDetailViewModel( loadParams = DataLoadParams(), listParams = EnrollmentDataSource.GetListParams( personUid = person.guid, - classUid = route.guid, + classUid = route.classActivityId, ) ).dataOrNull() ?: throw IllegalStateException() @@ -386,7 +407,7 @@ class ClazzDetailViewModel( EnrollmentList.create( filterByPersonUid = person.guid, role = role, - filterByClassUid = route.guid + filterByClassUid = route.classActivityId ) ) ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt index 01fc1f956..76f5cb84d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt @@ -10,13 +10,16 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.lib.dataloadstate.DataLoadParams +import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.school.ClassDataSource -import world.respect.datalayer.school.model.Clazz -import world.respect.datalayer.shared.paging.EmptyPagingSourceFactory -import world.respect.datalayer.shared.paging.IPagingSourceFactory -import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder +import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull +import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiStatement +import world.respect.lib.xapi.model.XapiVerb +import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.xapi.ACTIVITY_TYPE_CLASS +import world.respect.shared.domain.xapi.CATEGORY_CLASS_MANAGEMENT import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.classes import world.respect.shared.generated.resources.clazz @@ -34,7 +37,7 @@ import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState data class ClazzListUiState( - val classes: IPagingSourceFactory = EmptyPagingSourceFactory(), + val classStatements: List = emptyList(), val sortOptions: List = emptyList(), val activeSortOrderOption: SortOrderOption = SortOrderOption( Res.string.first_name, 1, true @@ -59,13 +62,6 @@ class ClazzListViewModel( private val enqueuePullSyncUseCase: EnqueueRunPullSyncUseCase by inject() - private val pagingSourceHolder = PagingSourceFactoryHolder { - schoolDataSource.classDataSource.listAsPagingSource( - loadParams = DataLoadParams(), - params = ClassDataSource.GetListParams() - ) - } - init { _appUiState.update { it.copy( @@ -81,7 +77,6 @@ class ClazzListViewModel( _uiState.update { prev -> prev.copy( - classes = pagingSourceHolder, sortOptions = listOf( SortOrderOption( Res.string.first_name, @@ -112,6 +107,30 @@ class ClazzListViewModel( ) } } + + viewModelScope.launch { + schoolDataSource.xapiStatementsResource.getAsFlow( + listParams = GetStatementParams( + activity = CATEGORY_CLASS_MANAGEMENT, + relatedActivities = true, + ), + dataLoadParams = DataLoadParams(), + ).collect { dataLoadState -> + val statements = dataLoadState.dataOrNull()?.statements + ?.filter { stmt -> + stmt.verb.id == XapiVerb.ID_SAVED + && (stmt.`object` as? XapiActivity)?.definition?.type == ACTIVITY_TYPE_CLASS + } + ?.groupBy { (it.`object` as? XapiActivity)?.id } + ?.mapNotNull { (_, stmts) -> + stmts.mostRecentByTimestampOrNull() + } ?: emptyList() + + _uiState.update { prev -> + prev.copy(classStatements = statements) + } + } + } } fun onSortOrderChanged(sortOption: SortOrderOption) { @@ -120,17 +139,20 @@ class ClazzListViewModel( } } - fun onClickClazz(clazz: Clazz) { + fun onClickClazz(statement: XapiStatement) { + val activityId = (statement.`object` as? XapiActivity)?.id ?: return _navCommandFlow.tryEmit( NavCommand.Navigate( - ClazzDetail(clazz.guid) + ClazzDetail( + classActivityId = activityId, + ) ) ) } fun onClickAdd() { _navCommandFlow.tryEmit( - NavCommand.Navigate(ClazzEdit(guid = null)) + NavCommand.Navigate(ClazzEdit(classActivityId = null)) ) } } From dc9da062060be247277046a98b4587b26adad87e Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 04:41:15 +0530 Subject: [PATCH 090/130] Refactor class management to support multi-language xAPI activity names and descriptions. - Replace custom `classDefinitionTitle` and `classDefinitionDescription` extensions with standard `objectActivityNameOrNull` and `objectActivityOrNull` from the xAPI library. - Update `ClazzEditScreen` to use `LangMapTextField` for editing localized name and description maps. - Update `ClazzDetailViewModel`, `ClazzListScreen`, and `ClazzDetailScreen` to handle language map conversions for UI display using `asLangMapUiText` and `langMapString`. - Remove obsolete xAPI helper functions and constants from `XapiClassExt.kt`. - Move `ACTIVITY_ID_PATH` to `ClazzEditViewModel` companion object. --- .../view/clazz/detail/ClazzDetailScreen.kt | 10 +++- .../app/view/clazz/edit/ClazzEditScreen.kt | 46 +++++++----------- .../app/view/clazz/list/ClazzListScreen.kt | 6 ++- .../shared/domain/xapi/XapiClassExt.kt | 48 ------------------- .../clazz/detail/ClazzDetailViewModel.kt | 10 ++-- .../clazz/edit/ClazzEditViewModel.kt | 13 +++-- 6 files changed, 42 insertions(+), 91 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index d697b2b3b..4297c33d9 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -32,10 +32,11 @@ import world.respect.app.components.RespectPersonAvatar import world.respect.app.components.respectPagingItems import world.respect.app.components.respectRememberPager import world.respect.datalayer.db.school.ext.fullName +import world.respect.app.components.langMapString import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Person -import world.respect.shared.domain.xapi.classDefinitionDescription +import world.respect.lib.xapi.ext.objectActivityOrNull import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_student import world.respect.shared.generated.resources.add_teacher @@ -129,7 +130,12 @@ fun ClazzDetailScreen( }, supportingContent = { - Text(text = uiState.classStatement.dataOrNull()?.classDefinitionDescription ?: "") + Text( + text = uiState.classStatement.dataOrNull()?.objectActivityOrNull() + ?.definition?.description?.takeIf { it.isNotEmpty() }?.let { + langMapString(it) + } ?: "", + ) } ) } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt index 5e6cf881b..a1037102c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/edit/ClazzEditScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -14,14 +13,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import kotlinx.coroutines.Dispatchers import org.jetbrains.compose.resources.stringResource +import world.respect.app.components.LangMapTextField import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource import world.respect.lib.dataloadstate.ext.dataOrNull +import world.respect.lib.xapi.ext.copyWithObjectActivityDescription +import world.respect.lib.xapi.ext.copyWithObjectActivityName +import world.respect.lib.xapi.ext.objectActivityOrNull import world.respect.lib.xapi.model.XapiStatement -import world.respect.shared.domain.xapi.classDefinitionTitle -import world.respect.shared.domain.xapi.classDefinitionDescription -import world.respect.shared.domain.xapi.withClassTitle -import world.respect.shared.domain.xapi.withClassDescription import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.class_name import world.respect.shared.generated.resources.description @@ -38,7 +37,6 @@ fun ClazzEditScreen( ClazzEditScreen( uiState = uiState, onEntityChanged = viewModel::onEntityChanged, - onClearError = viewModel::onClearError ) } @@ -46,7 +44,6 @@ fun ClazzEditScreen( fun ClazzEditScreen( uiState: ClazzEditUiState, onEntityChanged: (XapiStatement) -> Unit = {}, - onClearError: () -> Unit = {}, ) { val statement = uiState.statementData.dataOrNull() @@ -56,42 +53,31 @@ fun ClazzEditScreen( modifier = Modifier.fillMaxSize() .verticalScroll(rememberScrollState()) ) { - OutlinedTextField( + LangMapTextField( modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag("name"), - value = statement?.classDefinitionTitle ?: "", + value = statement?.objectActivityOrNull()?.definition?.name ?: emptyMap(), + onValueChange = { value -> + statement?.copyWithObjectActivityName(value)?.also { onEntityChanged(it) } + }, label = { Text(stringResource(Res.string.class_name) + "*") }, - onValueChange = { value -> - statement?.also { - onEntityChanged(it.withClassTitle(value)) - } - if (uiState.clazzNameError != null && value.isNotBlank()) { - onClearError() - } - }, - singleLine = true, supportingText = { Text(uiTextStringResource(uiState.clazzNameError ?: Res.string.required.asUiText())) }, enabled = fieldsEnabled, - isError = uiState.clazzNameError != null ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag(("description")), - value = statement?.classDefinitionDescription ?: "", + LangMapTextField( + modifier = Modifier.fillMaxWidth().defaultItemPadding().testTag("description"), + value = statement?.objectActivityOrNull()?.definition?.description ?: emptyMap(), label = { - Text( - stringResource(Res.string.description) - ) + Text(stringResource(Res.string.description)) }, - onValueChange = { newValue -> - statement?.also { - onEntityChanged(it.withClassDescription(newValue)) - } + onValueChange = { value -> + statement?.copyWithObjectActivityDescription(value)?.also { onEntityChanged(it) } }, - enabled = fieldsEnabled + enabled = fieldsEnabled, ) } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt index a000af04e..5f8fd9003 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt @@ -17,9 +17,10 @@ import world.respect.app.components.RespectPersonAvatar import world.respect.app.components.SortListMode import world.respect.app.components.defaultItemPadding import world.respect.app.components.defaultSortListMode +import world.respect.app.components.langMapString +import world.respect.lib.xapi.ext.objectActivityOrNull import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiStatement -import world.respect.shared.domain.xapi.classDefinitionTitle import world.respect.shared.util.SortOrderOption import world.respect.shared.viewmodel.clazz.list.ClazzListUiState import world.respect.shared.viewmodel.clazz.list.ClazzListViewModel @@ -65,7 +66,8 @@ fun ClazzListScreen( (statement.`object` as? XapiActivity)?.id ?: statement.id.toString() }, ) { statement -> - val title = statement.classDefinitionTitle + val title = statement.objectActivityOrNull()?.definition?.name + ?.takeIf { it.isNotEmpty() }?.let { langMapString(it) } ?:"" ListItem( modifier = Modifier .fillMaxWidth() diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt index 233686196..dafb4aba6 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/xapi/XapiClassExt.kt @@ -12,8 +12,6 @@ import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -const val XAPI_LANG_KEY = "en-US" - /** * Activity type for a class as per README_CLASS_RECIPE.md */ @@ -24,50 +22,6 @@ const val ACTIVITY_TYPE_CLASS = "http://id.openeel.org/xapi/activity-type/class" */ const val CATEGORY_CLASS_MANAGEMENT = "https://id.openeel.org/xapi/recipes/class-management" -const val ACTIVITY_ID_PATH = "xapi/activities/classes" - - -/** - * The activity definition name for a class statement. - */ -@OptIn(ExperimentalUuidApi::class) -val XapiStatement.classDefinitionTitle: String - get() = (this.`object` as? XapiActivity)?.definition?.name?.values?.firstOrNull() ?: "" - -/** - * The activity definition description for a class statement. - */ -val XapiStatement.classDefinitionDescription: String - get() = (this.`object` as? XapiActivity)?.definition?.description?.values?.firstOrNull() ?: "" - - -private fun XapiStatement.withDefinitionField( - update: (XapiActivityDefinition) -> XapiActivityDefinition -): XapiStatement { - val activity = `object` as? XapiActivity - ?: throw IllegalStateException("Statement object is not an XapiActivity") - val definition = activity.definition ?: XapiActivityDefinition() - return copy( - `object` = activity.copy(definition = update(definition)) - ) -} - -/** - * Create a copy of this class statement with an updated title in the activity definition name. - */ -fun XapiStatement.withClassTitle(title: String): XapiStatement = - withDefinitionField { def -> - def.copy(name = (def.name ?: emptyMap()) + (XAPI_LANG_KEY to title)) - } - -/** - * Create a copy of this class statement with an updated description in the activity definition. - */ -fun XapiStatement.withClassDescription(description: String): XapiStatement = - withDefinitionField { def -> - def.copy(description = (def.description ?: emptyMap()) + (XAPI_LANG_KEY to description)) - } - /** * Create a blank class statement as per the class-management recipe. @@ -88,8 +42,6 @@ fun createBlankClassStatement( objectType = XapiObjectType.Activity, id = classActivityId, definition = XapiActivityDefinition( - name = emptyMap(), - description = emptyMap(), type = ACTIVITY_TYPE_CLASS, ) ), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 472596bb0..98786b0e4 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -32,9 +32,9 @@ import world.respect.libutil.util.time.localDateInCurrentTimeZone import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.account.invite.ApproveOrDeclineInviteRequestUseCase import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator -import world.respect.shared.domain.xapi.classDefinitionTitle -import world.respect.shared.domain.xapi.classDefinitionDescription +import world.respect.lib.xapi.ext.objectActivityNameOrNull import world.respect.shared.ext.whenSubscribed +import world.respect.shared.util.ext.asLangMapUiText import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.first_name import world.respect.shared.generated.resources.last_name @@ -61,7 +61,6 @@ import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum import world.respect.datalayer.school.writequeue.EnqueueRunPullSyncUseCase import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull -import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams import world.respect.shared.domain.enrollments.UpdateClazzStudentXapiGroupUseCase @@ -208,7 +207,8 @@ class ClazzDetailViewModel( ?.mostRecentByTimestampOrNull() _appUiState.update { - it.copy(title = statement?.classDefinitionTitle?.asUiText()) + it.copy(title = statement?.objectActivityNameOrNull() + ?.takeIf { map -> map.isNotEmpty() }?.asLangMapUiText()) } _uiState.update { it.copy( @@ -279,7 +279,7 @@ class ClazzDetailViewModel( fun onClickAddPersonToClazz(roleType: EnrollmentRoleEnum) { viewModelScope.launch { val statement = _uiState.value.classStatement.dataOrNull() ?: return@launch - val classTitle = statement.classDefinitionTitle + val classTitle = statement.objectActivityNameOrNull()?.values?.firstOrNull() ?: "" _navCommandFlow.tryEmit( NavCommand.Navigate( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index 444e348b4..924ed6d96 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -21,12 +21,13 @@ import world.respect.lib.dataloadstate.ext.isReadyAndSettled import world.respect.lib.dataloadstate.ext.firstOrNotLoaded import world.respect.lib.dataloadstate.ext.map import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull +import world.respect.lib.xapi.ext.objectActivityNameOrNull +import world.respect.lib.xapi.ext.objectActivityOrNull import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams import world.respect.libutil.ext.appendEndpointSegments +import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.domain.xapi.ACTIVITY_ID_PATH -import world.respect.shared.domain.xapi.classDefinitionTitle import world.respect.shared.domain.xapi.createBlankClassStatement import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_clazz @@ -149,7 +150,7 @@ class ClazzEditViewModel( prev.copy( statementData = DataReadyState(statement), clazzNameError = prev.clazzNameError?.takeIf { - prev.statementData.dataOrNull()?.classDefinitionTitle == statement.classDefinitionTitle + prev.statementData.dataOrNull()?.objectActivityNameOrNull() == statement.objectActivityNameOrNull() }, ) } @@ -166,7 +167,7 @@ class ClazzEditViewModel( prev.copy( clazzNameError = Res.string.required_field.asUiText().takeIf { - statement?.classDefinitionTitle.isNullOrBlank() + statement?.objectActivityOrNull()?.definition?.name.isNullOrAllBlank() }, ) } @@ -205,4 +206,8 @@ class ClazzEditViewModel( fun onClearError() { _uiState.update { prev -> prev.copy(clazzNameError = null) } } + + companion object{ + const val ACTIVITY_ID_PATH = "xapi/activities/classes" + } } \ No newline at end of file From 3ff52665c0c961f6b6317908effe4d81a5064a5a Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 13:16:54 +0530 Subject: [PATCH 091/130] Use `objectActivityOrNull` and `distinctByMostRecentTimestampForActivityId` extensions in `ClazzListScreen` and `ClazzListViewModel`. --- .../app/view/clazz/list/ClazzListScreen.kt | 3 +-- .../viewmodel/clazz/list/ClazzListViewModel.kt | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt index 5f8fd9003..84f27a2b2 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/list/ClazzListScreen.kt @@ -19,7 +19,6 @@ import world.respect.app.components.defaultItemPadding import world.respect.app.components.defaultSortListMode import world.respect.app.components.langMapString import world.respect.lib.xapi.ext.objectActivityOrNull -import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiStatement import world.respect.shared.util.SortOrderOption import world.respect.shared.viewmodel.clazz.list.ClazzListUiState @@ -63,7 +62,7 @@ fun ClazzListScreen( items( items = uiState.classStatements, key = { statement -> - (statement.`object` as? XapiActivity)?.id ?: statement.id.toString() + statement.objectActivityOrNull()?.id ?: statement.id.toString() }, ) { statement -> val title = statement.objectActivityOrNull()?.definition?.name diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt index 76f5cb84d..840fffb70 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt @@ -12,8 +12,8 @@ import org.koin.core.scope.Scope import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.datalayer.SchoolDataSource -import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull -import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.ext.distinctByMostRecentTimestampForActivityId +import world.respect.lib.xapi.ext.objectActivityOrNull import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams @@ -119,12 +119,10 @@ class ClazzListViewModel( val statements = dataLoadState.dataOrNull()?.statements ?.filter { stmt -> stmt.verb.id == XapiVerb.ID_SAVED - && (stmt.`object` as? XapiActivity)?.definition?.type == ACTIVITY_TYPE_CLASS + && stmt.objectActivityOrNull()?.definition?.type == ACTIVITY_TYPE_CLASS } - ?.groupBy { (it.`object` as? XapiActivity)?.id } - ?.mapNotNull { (_, stmts) -> - stmts.mostRecentByTimestampOrNull() - } ?: emptyList() + ?.distinctByMostRecentTimestampForActivityId() + ?: emptyList() _uiState.update { prev -> prev.copy(classStatements = statements) @@ -140,7 +138,8 @@ class ClazzListViewModel( } fun onClickClazz(statement: XapiStatement) { - val activityId = (statement.`object` as? XapiActivity)?.id ?: return + val activityId = statement.objectActivityOrNull()?.id + ?: throw IllegalStateException("onClickClazz: statement object is not an Activity or has no id") _navCommandFlow.tryEmit( NavCommand.Navigate( ClazzDetail( From e57adfedf07a761ec49897216ba14fac71ac499a Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 13:28:29 +0530 Subject: [PATCH 092/130] Throw `IllegalStateException` in `ClazzEditViewModel` when account data is missing during class creation or when statement data is null during save. --- .../shared/viewmodel/clazz/edit/ClazzEditViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index 924ed6d96..8f2b7d074 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -130,7 +130,7 @@ class ClazzEditViewModel( ) } else { val actor = accountManager.selectedAccountAndPersonFlow.firstOrNull()?.xapiAgent - ?: return@launchWithLoadingIndicator + ?: throw IllegalStateException("ClazzEditViewModel: no active account/agent available to create a class") val baseStmt = createBlankClassStatement( classActivityId = classActivityId, actor = actor @@ -175,7 +175,8 @@ class ClazzEditViewModel( if (stateToSave.hasErrors) return - val classStatement = _uiState.value.statementData.dataOrNull() ?: return + val classStatement = _uiState.value.statementData.dataOrNull() + ?: throw IllegalStateException("onClickSave: statement data cannot be null after validation") launchWithLoadingIndicator { schoolDataSource.xapiStatementsResource.post( From e4c644bb9b01d4cc1c47bd68e8e6d3187f2061a7 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 13:44:51 +0530 Subject: [PATCH 093/130] Refine error handling in `onClickAddPersonToClazz` and `onClickRemovePersonFromClass` within `ClazzDetailViewModel`. --- .../clazz/detail/ClazzDetailViewModel.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 98786b0e4..768d4b59e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -3,7 +3,6 @@ package world.respect.shared.viewmodel.clazz.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -51,7 +50,6 @@ import world.respect.shared.navigation.PersonList import world.respect.shared.navigation.RouteResultDest import world.respect.shared.util.FilterChipsOption import world.respect.shared.util.SortOrderOption -import world.respect.shared.util.exception.getUiTextOrGeneric import world.respect.shared.util.ext.asUiText import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase.PermissionsRequiredByRole @@ -68,7 +66,6 @@ import world.respect.shared.domain.permissions.CheckSchoolPermissionsUseCase import world.respect.shared.ext.tryOrShowSnackbarOnError import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState -import world.respect.shared.viewmodel.app.appstate.Snack import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL import kotlin.getValue @@ -278,7 +275,8 @@ class ClazzDetailViewModel( fun onClickAddPersonToClazz(roleType: EnrollmentRoleEnum) { viewModelScope.launch { - val statement = _uiState.value.classStatement.dataOrNull() ?: return@launch + val statement = _uiState.value.classStatement.dataOrNull() + ?: throw IllegalStateException("onClickAddPersonToClazz: class statement must be loaded") val classTitle = statement.objectActivityNameOrNull()?.values?.firstOrNull() ?: "" _navCommandFlow.tryEmit( @@ -354,14 +352,14 @@ class ClazzDetailViewModel( fun onClickRemovePersonFromClass(person: Person, role: EnrollmentRoleEnum) { viewModelScope.launch { - try { val personEnrollments = schoolDataSource.enrollmentDataSource.list( loadParams = DataLoadParams(), listParams = EnrollmentDataSource.GetListParams( personUid = person.guid, classUid = route.classActivityId, ) - ).dataOrNull() ?: throw IllegalStateException() + ).dataOrNull() + ?: throw IllegalStateException("onClickRemovePersonFromClass: failed to load enrollments") val today = localDateInCurrentTimeZone() val modTime = Clock.System.now() @@ -384,12 +382,6 @@ class ClazzDetailViewModel( } schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) - - }catch(e: Throwable) { - //do something - Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) - snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) - } } } From a5db38cef14047703acc37ec92cbf226f40c03f3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 14:03:21 +0530 Subject: [PATCH 094/130] Simplify logic for title and description in `ClazzDetailViewModel` and `ClazzDetailScreen`. --- .../respect/app/view/clazz/detail/ClazzDetailScreen.kt | 6 ++++-- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 3 +-- .../shared/viewmodel/clazz/edit/ClazzEditViewModel.kt | 5 +---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 4297c33d9..08106f750 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -108,6 +108,9 @@ fun ClazzDetailScreen( val pendingTeacherLazyPagingItems = pendingTeacherPager.flow.collectAsLazyPagingItems() val pendingStudentLazyPagingItems = pendingStudentPager.flow.collectAsLazyPagingItems() + val classStatement = uiState.classStatement.dataOrNull() + + fun Person?.key(role: EnrollmentRoleEnum, index: Int): Any { return this?.guid?.let { Pair(it, role) @@ -131,8 +134,7 @@ fun ClazzDetailScreen( supportingContent = { Text( - text = uiState.classStatement.dataOrNull()?.objectActivityOrNull() - ?.definition?.description?.takeIf { it.isNotEmpty() }?.let { + text =classStatement?.objectActivityOrNull()?.definition?.description?.let { langMapString(it) } ?: "", ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 768d4b59e..2cb6794a6 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -204,8 +204,7 @@ class ClazzDetailViewModel( ?.mostRecentByTimestampOrNull() _appUiState.update { - it.copy(title = statement?.objectActivityNameOrNull() - ?.takeIf { map -> map.isNotEmpty() }?.asLangMapUiText()) + it.copy(title = statement?.objectActivityNameOrNull()?.asLangMapUiText()) } _uiState.update { it.copy( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index 8f2b7d074..ba1095176 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -77,10 +77,7 @@ class ClazzEditViewModel( private val schoolUrl = accountManager.requireActiveSchoolUrl() private val classActivityId = route.classActivityId ?: run { - schoolUrl.appendEndpointSegments( - ACTIVITY_ID_PATH, - Uuid.random().toString() - ).toString() + schoolUrl.appendEndpointSegments(ACTIVITY_ID_PATH, Uuid.random().toString()).toString() } private val debouncer = LaunchDebouncer(viewModelScope) From 7786c963f6d8a61e03b619d62df70f384e84325a Mon Sep 17 00:00:00 2001 From: mandviverma Date: Wed, 10 Jun 2026 14:04:45 +0530 Subject: [PATCH 095/130] Simplify logic for title and description in `ClazzDetailViewModel` and `ClazzDetailScreen`. --- .../world/respect/app/view/clazz/detail/ClazzDetailScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt index 08106f750..c384a62ab 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/clazz/detail/ClazzDetailScreen.kt @@ -110,7 +110,6 @@ fun ClazzDetailScreen( val classStatement = uiState.classStatement.dataOrNull() - fun Person?.key(role: EnrollmentRoleEnum, index: Int): Any { return this?.guid?.let { Pair(it, role) From 44e31ad672debd0e7712a2cd59745ad3eaa59264 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 11 Jun 2026 01:24:06 +0530 Subject: [PATCH 096/130] Add error handling and logging to `onClickRemovePersonFromClass` in `ClazzDetailViewModel`. --- .../viewmodel/clazz/detail/ClazzDetailViewModel.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 2cb6794a6..d440b157a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -3,6 +3,7 @@ package world.respect.shared.viewmodel.clazz.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -50,6 +51,7 @@ import world.respect.shared.navigation.PersonList import world.respect.shared.navigation.RouteResultDest import world.respect.shared.util.FilterChipsOption import world.respect.shared.util.SortOrderOption +import world.respect.shared.util.exception.getUiTextOrGeneric import world.respect.shared.util.ext.asUiText import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase.PermissionsRequiredByRole @@ -66,6 +68,7 @@ import world.respect.shared.domain.permissions.CheckSchoolPermissionsUseCase import world.respect.shared.ext.tryOrShowSnackbarOnError import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState +import world.respect.shared.viewmodel.app.appstate.Snack import world.respect.shared.viewmodel.app.appstate.SnackBarDispatcher import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel.Companion.ALL import kotlin.getValue @@ -351,6 +354,7 @@ class ClazzDetailViewModel( fun onClickRemovePersonFromClass(person: Person, role: EnrollmentRoleEnum) { viewModelScope.launch { + try { val personEnrollments = schoolDataSource.enrollmentDataSource.list( loadParams = DataLoadParams(), listParams = EnrollmentDataSource.GetListParams( @@ -381,6 +385,12 @@ class ClazzDetailViewModel( } schoolDataSource.enrollmentDataSource.store(enrollmentsToStore) + + }catch(e: Throwable) { + //do something + Napier.e("onClickRemovePersonFromClass ERROR", throwable = e) + snackBarDispatcher.showSnackBar(Snack(e.getUiTextOrGeneric())) + } } } From 1c2f8f8cefa50c63bf814360ef37dbbb569a0f16 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 11 Jun 2026 11:46:05 +0530 Subject: [PATCH 097/130] Update `CreateClassUseCase` to focus on invite creation and use it in `ClazzEditViewModel`. --- .../domain/createclass/CreateClassUseCase.kt | 21 ++++++++++--------- .../clazz/edit/ClazzEditViewModel.kt | 5 +++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt index ce995e8f8..4ca651e32 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt @@ -3,27 +3,28 @@ package world.respect.shared.domain.createclass import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.school.model.ClassInvite import world.respect.datalayer.school.model.ClassInviteModeEnum -import world.respect.datalayer.school.model.Clazz import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.Invite2 /** - * Use case to contain logic for creating a new class. + * Use case to contain logic for creating invites for a new class. * - * Currently: + * Creates invites for each role (teacher direct, student direct, student via parent). * - * 1) Stores the class itself - * 2) Stores invites for the class for each role + * @property dataSource the SchoolDataSource used to store invites */ class CreateClassUseCase( private val dataSource: SchoolDataSource ) { + /** + * Create invites for a class identified by its activity ID. + * + * @param classActivityId the xAPI activity ID of the class + */ suspend operator fun invoke( - clazz: Clazz + classActivityId: String ) { - dataSource.classDataSource.store(listOf(clazz)) - dataSource.inviteDataSource.store( listOf( Pair(EnrollmentRoleEnum.TEACHER, ClassInviteModeEnum.DIRECT), @@ -32,10 +33,10 @@ class CreateClassUseCase( ).map { (role, inviteMode) -> ClassInvite( uid = ClassInvite.uidFor( - clazz.guid, role, inviteMode = inviteMode + classActivityId, role, inviteMode = inviteMode ), code = Invite2.newRandomCode(), - classUid = clazz.guid, + classUid = classActivityId, role = role, inviteMode = inviteMode, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index ba1095176..23fb52733 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -28,6 +28,7 @@ import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParam import world.respect.libutil.ext.appendEndpointSegments import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.createclass.CreateClassUseCase import world.respect.shared.domain.xapi.createBlankClassStatement import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_clazz @@ -68,6 +69,7 @@ class ClazzEditViewModel( override val scope: Scope = accountManager.requireActiveAccountScope() private val schoolDataSource: SchoolDataSource by inject() + private val createClassUseCase: CreateClassUseCase by inject() private val route: ClazzEdit = savedStateHandle.toRoute() private val _uiState = MutableStateFlow(ClazzEditUiState()) @@ -186,6 +188,9 @@ class ClazzEditViewModel( ) if (route.classActivityId == null) { + // Create invites for the new class + createClassUseCase(classActivityId) + _navCommandFlow.tryEmit( NavCommand.Navigate( destination = ClazzDetail( From 0609fc9d32ee5c3dd554a4f5d52df13bb7d3d4be Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 11 Jun 2026 12:07:06 +0530 Subject: [PATCH 098/130] Update `CreateClassUseCase` to focus on invite creation and use it in `ClazzEditViewModel`. --- .../respect/shared/domain/createclass/CreateClassUseCase.kt | 6 ------ .../shared/viewmodel/clazz/edit/ClazzEditViewModel.kt | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt index 4ca651e32..e295c6e98 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/createclass/CreateClassUseCase.kt @@ -16,12 +16,6 @@ import world.respect.datalayer.school.model.Invite2 class CreateClassUseCase( private val dataSource: SchoolDataSource ) { - - /** - * Create invites for a class identified by its activity ID. - * - * @param classActivityId the xAPI activity ID of the class - */ suspend operator fun invoke( classActivityId: String ) { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index 23fb52733..5cc44eacc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -188,7 +188,7 @@ class ClazzEditViewModel( ) if (route.classActivityId == null) { - // Create invites for the new class + createClassUseCase(classActivityId) _navCommandFlow.tryEmit( From f15772c618b4d5abb87d4bab9861b6b4aaadb62f Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 03:03:17 +0530 Subject: [PATCH 099/130] Assign to students --- ...er_assigns_assignment_to_a_class_test.yaml | 13 +++------- .../assignment/edit/AssignmentEditScreen.kt | 12 ++++----- .../edit/AssignmentEditViewModel.kt | 25 +++++++++++-------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 757b508d2..10eeac92f 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -47,7 +47,7 @@ onFlowComplete: - tapOn: "Assignment title*" - inputText: "Homework 1" - tapOn: "Assign to" -- tapOn: "TestClass" +- tapOn: "StudentA User" - tapOn: "Description" - inputText: "Please complete this Homework on time." - runFlow: @@ -92,15 +92,12 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Homework 1" -- assertVisible: "All (3)" +- assertVisible: "All (1)" - assertVisible: "Completed (0)" - assertVisible: "In-progress (0)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" -- assertVisible: "TestClass.*" - assertVisible: "StudentA User" -- assertVisible: "StudentB User" -- assertVisible: "StudentC User" - assertVisible: "Statement Maker" - assertVisible: "Assignment Result Statement Demo" - assertVisible: "Average" @@ -179,19 +176,15 @@ onFlowComplete: PASSWORD: "test123" - tapOn: "Assignments" - assertVisible: "Homework 1" -- assertVisible: "TestClass.*" - tapOn: "Homework 1" -- assertVisible: "All (3)" +- assertVisible: "All (1)" - assertVisible: "Completed (1)" - assertVisible: "In-progress (1)" # 22/May: Disabled: needs horizontal scroll MD #- assertVisible: "Not started (1)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" -- assertVisible: "TestClass.*" - assertVisible: "StudentA User" -- assertVisible: "StudentB User" -- assertVisible: "StudentC User" - assertVisible: "Statement Maker" - assertVisible: "Average" - assertVisible: "95%" diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index e5e6e3d4b..c8b117fe2 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -44,7 +44,7 @@ import world.respect.app.components.LangMapTextField import world.respect.app.components.RespectLocalDateTimeField import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource -import world.respect.datalayer.school.model.Clazz +import world.respect.datalayer.school.model.Person import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.xapi.ext.copyWithObjectActivityDescription import world.respect.lib.xapi.ext.copyWithObjectActivityName @@ -80,7 +80,7 @@ fun AssignmentEditScreen( onEntityChanged = viewModel::onEntityChanged, onAssigneeTextChanged = viewModel::onAssigneeTextChanged, onClickAddLearningUnit = viewModel::onClickAddLearningUnit, - onAssigneeClassSelected = viewModel::onAssigneeClassSelected, + onAssigneeStudentSelected = viewModel::onAssigneeStudentSelected, onClickRemoveLearningUnit = viewModel::onClickRemoveLearningUnit, ) } @@ -91,7 +91,7 @@ fun AssignmentEditScreen( uiState: AssignmentEditUiState, onEntityChanged: (XapiStatement) -> Unit, onAssigneeTextChanged: (String) -> Unit, - onAssigneeClassSelected: (Clazz) -> Unit, + onAssigneeStudentSelected: (Person) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (XapiActivity) -> Unit, ) { @@ -153,14 +153,14 @@ fun AssignmentEditScreen( expanded = expanded, onDismissRequest = { expanded = false } ) { - uiState.classOptions.forEach { clazz -> + uiState.studentOptions.forEach { student -> DropdownMenuItem( text = { - Text(clazz.title) + Text("${student.givenName} ${student.familyName}") }, onClick = { expanded = false - onAssigneeClassSelected(clazz) + onAssigneeStudentSelected(student) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index b034b8a80..7fba3947d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -17,8 +17,10 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.school.ClassDataSource -import world.respect.datalayer.school.model.Clazz +import world.respect.datalayer.school.PersonDataSource +import world.respect.datalayer.school.ext.asXapiAgent +import world.respect.datalayer.school.model.PersonRoleEnum +import world.respect.datalayer.school.model.Person import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.DataLoadState import world.respect.lib.dataloadstate.DataLoadingState @@ -42,7 +44,6 @@ import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.opds.getxapiactivityid.GetXapiActivityForPublicationUseCase import world.respect.shared.domain.xapi.createBlankAssignmentStatement -import world.respect.shared.ext.studentsXapiGroup import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_assignment import world.respect.shared.generated.resources.edit_assignment @@ -70,7 +71,7 @@ data class AssignmentEditUiState( val statementData: DataLoadState = DataLoadingState(), val assignee: String = "", val nameError: UiText? = null, - val classOptions: List = emptyList(), + val studentOptions: List = emptyList(), val classError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, ) { @@ -135,14 +136,16 @@ class AssignmentEditViewModel( launchWithLoadingIndicator( onShowError = { snackBarDispatcher.showSnackBar(Snack(it)) } ) { - val classes = schoolDataSource.classDataSource.list( - DataLoadParams(), - ClassDataSource.GetListParams() + val students = schoolDataSource.personDataSource.list( + loadParams = DataLoadParams(), + params = PersonDataSource.GetListParams( + filterByPersonRole = PersonRoleEnum.STUDENT, + ) ).dataOrNull() ?: emptyList() _uiState.update { it.copy( - classOptions = classes, + studentOptions = students, ) } @@ -218,14 +221,14 @@ class AssignmentEditViewModel( ) } - fun onAssigneeClassSelected(clazz: Clazz) { + fun onAssigneeStudentSelected(person: Person) { val statement = _uiState.value.statementData.dataOrNull() ?: return _uiState.update { it.copy( statementData = DataReadyState( - statement.copy(actor = clazz.studentsXapiGroup(schoolUrl)) + statement.copy(actor = person.asXapiAgent(schoolUrl)) ), - assignee = clazz.title, + assignee = "${person.givenName} ${person.familyName}", classError = null, ) } From 2ac497f4c4c02bb3b160f8e7d9565e942bb97454 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 03:21:20 +0530 Subject: [PATCH 100/130] Update `AssignmentEditViewModel` and screen to support selecting classes as assignees instead of individual students. --- .../assignment/edit/AssignmentEditScreen.kt | 12 ++++----- .../edit/AssignmentEditViewModel.kt | 25 ++++++++----------- .../clazz/detail/ClazzDetailViewModel.kt | 4 +-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index c8b117fe2..e5e6e3d4b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -44,7 +44,7 @@ import world.respect.app.components.LangMapTextField import world.respect.app.components.RespectLocalDateTimeField import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource -import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.model.Clazz import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.xapi.ext.copyWithObjectActivityDescription import world.respect.lib.xapi.ext.copyWithObjectActivityName @@ -80,7 +80,7 @@ fun AssignmentEditScreen( onEntityChanged = viewModel::onEntityChanged, onAssigneeTextChanged = viewModel::onAssigneeTextChanged, onClickAddLearningUnit = viewModel::onClickAddLearningUnit, - onAssigneeStudentSelected = viewModel::onAssigneeStudentSelected, + onAssigneeClassSelected = viewModel::onAssigneeClassSelected, onClickRemoveLearningUnit = viewModel::onClickRemoveLearningUnit, ) } @@ -91,7 +91,7 @@ fun AssignmentEditScreen( uiState: AssignmentEditUiState, onEntityChanged: (XapiStatement) -> Unit, onAssigneeTextChanged: (String) -> Unit, - onAssigneeStudentSelected: (Person) -> Unit, + onAssigneeClassSelected: (Clazz) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (XapiActivity) -> Unit, ) { @@ -153,14 +153,14 @@ fun AssignmentEditScreen( expanded = expanded, onDismissRequest = { expanded = false } ) { - uiState.studentOptions.forEach { student -> + uiState.classOptions.forEach { clazz -> DropdownMenuItem( text = { - Text("${student.givenName} ${student.familyName}") + Text(clazz.title) }, onClick = { expanded = false - onAssigneeStudentSelected(student) + onAssigneeClassSelected(clazz) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 7fba3947d..b034b8a80 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -17,10 +17,8 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.school.PersonDataSource -import world.respect.datalayer.school.ext.asXapiAgent -import world.respect.datalayer.school.model.PersonRoleEnum -import world.respect.datalayer.school.model.Person +import world.respect.datalayer.school.ClassDataSource +import world.respect.datalayer.school.model.Clazz import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.DataLoadState import world.respect.lib.dataloadstate.DataLoadingState @@ -44,6 +42,7 @@ import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.opds.getxapiactivityid.GetXapiActivityForPublicationUseCase import world.respect.shared.domain.xapi.createBlankAssignmentStatement +import world.respect.shared.ext.studentsXapiGroup import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_assignment import world.respect.shared.generated.resources.edit_assignment @@ -71,7 +70,7 @@ data class AssignmentEditUiState( val statementData: DataLoadState = DataLoadingState(), val assignee: String = "", val nameError: UiText? = null, - val studentOptions: List = emptyList(), + val classOptions: List = emptyList(), val classError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, ) { @@ -136,16 +135,14 @@ class AssignmentEditViewModel( launchWithLoadingIndicator( onShowError = { snackBarDispatcher.showSnackBar(Snack(it)) } ) { - val students = schoolDataSource.personDataSource.list( - loadParams = DataLoadParams(), - params = PersonDataSource.GetListParams( - filterByPersonRole = PersonRoleEnum.STUDENT, - ) + val classes = schoolDataSource.classDataSource.list( + DataLoadParams(), + ClassDataSource.GetListParams() ).dataOrNull() ?: emptyList() _uiState.update { it.copy( - studentOptions = students, + classOptions = classes, ) } @@ -221,14 +218,14 @@ class AssignmentEditViewModel( ) } - fun onAssigneeStudentSelected(person: Person) { + fun onAssigneeClassSelected(clazz: Clazz) { val statement = _uiState.value.statementData.dataOrNull() ?: return _uiState.update { it.copy( statementData = DataReadyState( - statement.copy(actor = person.asXapiAgent(schoolUrl)) + statement.copy(actor = clazz.studentsXapiGroup(schoolUrl)) ), - assignee = "${person.givenName} ${person.familyName}", + assignee = clazz.title, classError = null, ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 662e738e1..8a2238877 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -139,7 +139,7 @@ class ClazzDetailViewModel( private val schoolSelfUrl = accountManager.activeAccount?.school?.self ?: throw IllegalStateException("schoolSelfUrl is required but activeAccount or school is null") - private val classActivityId = "${schoolSelfUrl}${CLASS}${route.guid}" + private val classActivityId = "${schoolSelfUrl}${CLASS}${route.classActivityId}" private fun pagingSourceByRole(role: EnrollmentRoleEnum): PagingSourceFactoryHolder { return PagingSourceFactoryHolder { @@ -433,7 +433,7 @@ class ClazzDetailViewModel( _navCommandFlow.tryEmit( NavCommand.Navigate( StudentGroupingEdit( - classUid = route.guid, groupId = null + classUid = route.classActivityId, groupId = null ) ) ) From 24d0da26c22e740d300effa2b902bcc3894780ff Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 03:22:09 +0530 Subject: [PATCH 101/130] Update assignment test flow to assign to a class and verify multiple student recipients. --- ...min_user_assigns_assignment_to_a_class_test.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 10eeac92f..757b508d2 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -47,7 +47,7 @@ onFlowComplete: - tapOn: "Assignment title*" - inputText: "Homework 1" - tapOn: "Assign to" -- tapOn: "StudentA User" +- tapOn: "TestClass" - tapOn: "Description" - inputText: "Please complete this Homework on time." - runFlow: @@ -92,12 +92,15 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Homework 1" -- assertVisible: "All (1)" +- assertVisible: "All (3)" - assertVisible: "Completed (0)" - assertVisible: "In-progress (0)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" +- assertVisible: "TestClass.*" - assertVisible: "StudentA User" +- assertVisible: "StudentB User" +- assertVisible: "StudentC User" - assertVisible: "Statement Maker" - assertVisible: "Assignment Result Statement Demo" - assertVisible: "Average" @@ -176,15 +179,19 @@ onFlowComplete: PASSWORD: "test123" - tapOn: "Assignments" - assertVisible: "Homework 1" +- assertVisible: "TestClass.*" - tapOn: "Homework 1" -- assertVisible: "All (1)" +- assertVisible: "All (3)" - assertVisible: "Completed (1)" - assertVisible: "In-progress (1)" # 22/May: Disabled: needs horizontal scroll MD #- assertVisible: "Not started (1)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" +- assertVisible: "TestClass.*" - assertVisible: "StudentA User" +- assertVisible: "StudentB User" +- assertVisible: "StudentC User" - assertVisible: "Statement Maker" - assertVisible: "Average" - assertVisible: "95%" From b4509d5eb00dc5a4f1961f5f06840b8a24581078 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 04:18:23 +0530 Subject: [PATCH 102/130] merged the student grouping code and now in assignment can be assigned to groups --- ...er_assigns_assignment_to_a_class_test.yaml | 26 +++++++-- .../assignment/edit/AssignmentEditScreen.kt | 16 +++--- .../edit/AssignmentEditViewModel.kt | 57 +++++++++++++------ 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 757b508d2..48a2c331a 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -23,6 +23,24 @@ onFlowComplete: env: CLASSNAME: "TestClass" +- tapOn: "Classes" +- tapOn: "TestClass" +- scrollUntilVisible: + element: "Create group" + direction: DOWN +- tapOn: "Create group" +- assertVisible: + id: "app_title" + text: "Create group" +- tapOn: + id: "name" +- inputText: "TestGroup" +- hideKeyboard +- tapOn: "StudentA User" +- tapOn: "StudentB User" +- tapOn: "StudentC User" +- tapOn: "Save" + # Teacher Add Assignment - clearState: world.respect.app - runFlow: @@ -47,7 +65,7 @@ onFlowComplete: - tapOn: "Assignment title*" - inputText: "Homework 1" - tapOn: "Assign to" -- tapOn: "TestClass" +- tapOn: "TestGroup" - tapOn: "Description" - inputText: "Please complete this Homework on time." - runFlow: @@ -97,7 +115,7 @@ onFlowComplete: - assertVisible: "In-progress (0)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" -- assertVisible: "TestClass.*" +- assertVisible: "TestGroup.*" - assertVisible: "StudentA User" - assertVisible: "StudentB User" - assertVisible: "StudentC User" @@ -179,7 +197,7 @@ onFlowComplete: PASSWORD: "test123" - tapOn: "Assignments" - assertVisible: "Homework 1" -- assertVisible: "TestClass.*" +- assertVisible: "TestGroup.*" - tapOn: "Homework 1" - assertVisible: "All (3)" - assertVisible: "Completed (1)" @@ -188,7 +206,7 @@ onFlowComplete: #- assertVisible: "Not started (1)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" -- assertVisible: "TestClass.*" +- assertVisible: "TestGroup.*" - assertVisible: "StudentA User" - assertVisible: "StudentB User" - assertVisible: "StudentC User" diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index e5e6e3d4b..492fe0b91 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -44,7 +44,7 @@ import world.respect.app.components.LangMapTextField import world.respect.app.components.RespectLocalDateTimeField import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource -import world.respect.datalayer.school.model.Clazz +import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.xapi.ext.copyWithObjectActivityDescription import world.respect.lib.xapi.ext.copyWithObjectActivityName @@ -80,7 +80,7 @@ fun AssignmentEditScreen( onEntityChanged = viewModel::onEntityChanged, onAssigneeTextChanged = viewModel::onAssigneeTextChanged, onClickAddLearningUnit = viewModel::onClickAddLearningUnit, - onAssigneeClassSelected = viewModel::onAssigneeClassSelected, + onAssigneeGroupSelected = viewModel::onAssigneeGroupSelected, onClickRemoveLearningUnit = viewModel::onClickRemoveLearningUnit, ) } @@ -91,7 +91,7 @@ fun AssignmentEditScreen( uiState: AssignmentEditUiState, onEntityChanged: (XapiStatement) -> Unit, onAssigneeTextChanged: (String) -> Unit, - onAssigneeClassSelected: (Clazz) -> Unit, + onAssigneeGroupSelected: (XapiGroup) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (XapiActivity) -> Unit, ) { @@ -143,9 +143,9 @@ fun AssignmentEditScreen( }, colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), supportingText = { - Text(uiTextStringResource(uiState.classError ?: Res.string.required.asUiText())) + Text(uiTextStringResource(uiState.groupError ?: Res.string.required.asUiText())) }, - isError = uiState.classError != null, + isError = uiState.groupError != null, ) ExposedDropdownMenu( @@ -153,14 +153,14 @@ fun AssignmentEditScreen( expanded = expanded, onDismissRequest = { expanded = false } ) { - uiState.classOptions.forEach { clazz -> + uiState.groupOptions.forEach { group -> DropdownMenuItem( text = { - Text(clazz.title) + Text(group.name ?: "") }, onClick = { expanded = false - onAssigneeClassSelected(clazz) + onAssigneeGroupSelected(group) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index b034b8a80..7b7a41c2b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -17,9 +17,9 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.school.ClassDataSource -import world.respect.datalayer.school.model.Clazz import world.respect.lib.dataloadstate.DataLoadParams +import world.respect.lib.xapi.model.VERB_SAVED +import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.dataloadstate.DataLoadState import world.respect.lib.dataloadstate.DataLoadingState import world.respect.lib.dataloadstate.DataReadyState @@ -42,8 +42,8 @@ import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.opds.getxapiactivityid.GetXapiActivityForPublicationUseCase import world.respect.shared.domain.xapi.createBlankAssignmentStatement -import world.respect.shared.ext.studentsXapiGroup import world.respect.shared.generated.resources.Res +import kotlin.time.Instant import world.respect.shared.generated.resources.add_assignment import world.respect.shared.generated.resources.edit_assignment import world.respect.shared.generated.resources.required_field @@ -70,15 +70,15 @@ data class AssignmentEditUiState( val statementData: DataLoadState = DataLoadingState(), val assignee: String = "", val nameError: UiText? = null, - val classOptions: List = emptyList(), - val classError: UiText? = null, + val groupOptions: List = emptyList(), + val groupError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, ) { val fieldsEnabled: Boolean get() = statementData.isReadyAndSettled() val hasErrors: Boolean - get() = nameError != null || classError != null + get() = nameError != null || groupError != null } @OptIn(ExperimentalUuidApi::class) @@ -135,14 +135,32 @@ class AssignmentEditViewModel( launchWithLoadingIndicator( onShowError = { snackBarDispatcher.showSnackBar(Snack(it)) } ) { - val classes = schoolDataSource.classDataSource.list( - DataLoadParams(), - ClassDataSource.GetListParams() - ).dataOrNull() ?: emptyList() + val statementResult = schoolDataSource.xapiStatementsResource.get( + listParams = GetStatementParams( + verb = VERB_SAVED, + ), + dataLoadParams = DataLoadParams() + ).dataOrNull() + + val allGroups = statementResult?.statements + ?.filter { it.verb.id == VERB_SAVED } + ?.sortedByDescending { it.timestamp ?: it.stored ?: Instant.DISTANT_PAST } + ?.mapNotNull { it.`object` as? XapiGroup } + ?.filter { group -> + group.account?.name != null && + group.account?.homePage == schoolUrl.toString() + } + ?.distinctBy { it.account?.name } + ?.map { group -> + val filteredMembers = group.member?.filter { agent -> + agent.name != null + } + group.copy(member = filteredMembers) + } ?: emptyList() _uiState.update { it.copy( - classOptions = classes, + groupOptions = allGroups, ) } @@ -218,15 +236,20 @@ class AssignmentEditViewModel( ) } - fun onAssigneeClassSelected(clazz: Clazz) { + fun onAssigneeGroupSelected(group: XapiGroup) { val statement = _uiState.value.statementData.dataOrNull() ?: return _uiState.update { it.copy( statementData = DataReadyState( - statement.copy(actor = clazz.studentsXapiGroup(schoolUrl)) + statement.copy( + actor = XapiGroup( + name = group.name, + account = group.account, + ) + ) ), - assignee = clazz.title, - classError = null, + assignee = group.name ?: "", + groupError = null, ) } } @@ -249,7 +272,7 @@ class AssignmentEditViewModel( fun onAssigneeTextChanged(text: String) { _uiState.update { - it.copy(assignee = text, classError = null) + it.copy(assignee = text, groupError = null) } } @@ -291,7 +314,7 @@ class AssignmentEditViewModel( nameError = Res.string.required_field.asUiText().takeIf { statement?.objectActivityOrNull()?.definition?.name.isNullOrAllBlank() }, - classError = Res.string.required_field.asUiText().takeIf { + groupError = Res.string.required_field.asUiText().takeIf { statement?.actor?.name.isNullOrEmpty() } ) From 31173d62c8b81a7f779664ef8df51d91d9c13e00 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 04:19:54 +0530 Subject: [PATCH 103/130] Update --- ...01_001_invite_users_using_qr_code_or_link_test.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml index f205000d7..94623f25a 100644 --- a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml +++ b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml @@ -105,10 +105,10 @@ onFlowComplete: - runFlow: file: "subflows/admin_add_class.yaml" env: - CLASSNAME: "TestClass" + CLASSNAME: "New Class" - assertVisible: id: "app_title" - text: "TestClass" + text: "New Class" - tapOn: "Add Student" - tapOn: "Invite person" - assertVisible: @@ -140,7 +140,7 @@ onFlowComplete: - assertVisible: "School name" - assertVisible: ${output.SCHOOL_NAME} - assertVisible: "Class name" -- assertVisible: "TestClass" +- assertVisible: "New Class" - assertVisible: "School server URL" - assertVisible: ${output.SCHOOL_URL} - tapOn: "Next" @@ -195,7 +195,7 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Classes" -- tapOn: "TestClass" +- tapOn: "New Class" - assertVisible: "Pending requests.*" - assertVisible: "Student User.*" - tapOn: "Accept Invite" @@ -223,7 +223,7 @@ onFlowComplete: - assertVisible: "School name" - assertVisible: ${output.SCHOOL_NAME} - assertVisible: "Class name" -- assertVisible: "TestClass" +- assertVisible: "New Class" - assertVisible: "School server URL" - assertVisible: ${output.SCHOOL_URL} - tapOn: "Next" From 8da297a28eaecf20052b9dd9c9ba1b6d514eb731 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 04:21:03 +0530 Subject: [PATCH 104/130] Update --- .../004_001_add_groups_to_a_class_test.yaml | 98 ------------------- 1 file changed, 98 deletions(-) delete mode 100644 .maestro/flows/004_001_add_groups_to_a_class_test.yaml diff --git a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml b/.maestro/flows/004_001_add_groups_to_a_class_test.yaml deleted file mode 100644 index 5353becc2..000000000 --- a/.maestro/flows/004_001_add_groups_to_a_class_test.yaml +++ /dev/null @@ -1,98 +0,0 @@ -appId: world.respect.app -onFlowStart: - - clearState: world.respect.app - - runScript: - file: "scripts/school_init.js" - env: - TESTCONTROLLER_URL: ${TESTCONTROLLER_URL} - SCHOOL_ADMIN_PASSWORD: ${SCHOOL_ADMIN_PASSWORD} - DIR_ADMIN_AUTH_HEADER: ${DIR_ADMIN_AUTH_HEADER} - SCHOOL_URL: ${SCHOOL_URL} - SCHOOL_NAME: ${SCHOOL_NAME} - URL_SUBSTITUTION: ${URL_SUBSTITUTION} - NAME: "004_001_add_groups_to_a_class_test" - -onFlowComplete: - - runScript: - file: "scripts/teardown.js" ---- -- runFlow: "subflows/school_admin_login_flow.yaml" -- runFlow: - file: "subflows/admin_add_class_with_members.yaml" - env: - CLASSNAME: "TestClass" -# -- clearState: world.respect.app -- launchApp: - arguments: - respect_directory: ${output.SCHOOL_URL} -- tapOn: "Get Started" -- runFlow: - file: "subflows/get_started_select_school_by_name.yaml" - env: - SCHOOL_NAME: ${SCHOOL_NAME} -- tapOn: - id: "username" -- inputText: "teacherauser" -- tapOn: - id : "password" -- inputText: "test123" -- tapOn: "Login" -- runFlow: - when: - visible: "Save password for Respect?" - file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" -- tapOn: "Classes" -- assertVisible: - id: "app_title" - text: "Classes" -- assertVisible: "TestClass" -- tapOn: "TestClass" -- assertVisible: - id: "app_title" - text: "TestClass" -- scrollUntilVisible: - element: "Groups" - direction: DOWN -- tapOn: "Create group" -- assertVisible: - id: "app_title" - text: "Create group" -- tapOn: "Name*" -- inputText: "Test Group" -- tapOn: "StudentA USER" -- tapOn: "StudentC USER" -- tapOn: "Save" -- assertVisible: - id: "app_title" - text: "Test Group" -- assertVisible: "StudentA USER" -- assertVisible: "StudentC USER" -- assertVisible: "2 Students" -- tapOn: - id: "delete_group_btn" -- assertVisible: "Permanently delete?" -- assertVisible: "Permanently delete this group" -- assertVisible: "Cancel" -- assertVisible: "Delete" -- tapOn: "Cancel" -- assertVisible: - id: "delete_group_btn" -- tapOn: - id: "floating_action_button" -- tapOn: "StudentB USER" -- tapOn: "Save" -- assertVisible: - id: "app_title" - text: "Test Group" -- assertVisible: "StudentB USER" -- assertVisible: "3 Students" -- back -- assertVisible: - id: "app_title" - text: "TestClass" -- scrollUntilVisible: - element: "Test Group (3)" -- assertVisible: "Groups (1)" -- assertVisible: "Test Group (3)" From b598175c4328fe319f7ecd73f551d59f3c6a9104 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 04:23:59 +0530 Subject: [PATCH 105/130] Update --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/flows/{ => test}/001_002_add_user_direct_test.yaml | 0 .../flows/{ => test}/001_003_login_using_school_link_test.yaml | 0 .maestro/flows/{ => test}/002_browse_lessons_test.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/flows/{ => test}/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/flows/{ => test}/001_002_add_user_direct_test.yaml (100%) rename .maestro/flows/{ => test}/001_003_login_using_school_link_test.yaml (100%) rename .maestro/flows/{ => test}/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows/001_002_add_user_direct_test.yaml b/.maestro/flows/test/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows/001_002_add_user_direct_test.yaml rename to .maestro/flows/test/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows/001_003_login_using_school_link_test.yaml b/.maestro/flows/test/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows/001_003_login_using_school_link_test.yaml rename to .maestro/flows/test/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/test/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows/002_browse_lessons_test.yaml rename to .maestro/flows/test/002_browse_lessons_test.yaml From 30b6bda80a3dd8dbcfb301ff8d213c5574afa87d Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 04:31:28 +0530 Subject: [PATCH 106/130] Update `schoolDataSource` access to use `xapiResource.statements` across various ViewModels. --- .../viewmodel/assignment/edit/AssignmentEditViewModel.kt | 2 +- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 2 +- .../shared/viewmodel/clazz/edit/ClazzEditViewModel.kt | 2 +- .../shared/viewmodel/clazz/list/ClazzListViewModel.kt | 2 +- .../detail/StudentGroupingDetailViewModel.kt | 2 +- .../studentgrouping/edit/StudentGroupingEditViewModel.kt | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index bc55ca62f..5ada69881 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -135,7 +135,7 @@ class AssignmentEditViewModel( launchWithLoadingIndicator( onShowError = { snackBarDispatcher.showSnackBar(Snack(it)) } ) { - val statementResult = schoolDataSource.xapiStatementsResource.get( + val statementResult = schoolDataSource.xapiResource.statements.get( listParams = GetStatementParams( verb = VERB_SAVED, ), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 8a2238877..f54603153 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -454,7 +454,7 @@ class ClazzDetailViewModel( @OptIn(kotlin.uuid.ExperimentalUuidApi::class) private suspend fun observeGroupsFromXapi() { - schoolDataSource.xapiStatementsResource.getAsFlow( + schoolDataSource.xapiResource.statements.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, activity = classActivityId, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index 5cc44eacc..ff129ecd7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -108,7 +108,7 @@ class ClazzEditViewModel( json = json, serializer = XapiStatement.serializer(), loadFn = { params -> - schoolDataSource.xapiStatementsResource.get( + schoolDataSource.xapiResource.statements.get( listParams = GetStatementParams( activity = classActivityId, ), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt index 840fffb70..28f7a7a5d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/list/ClazzListViewModel.kt @@ -109,7 +109,7 @@ class ClazzListViewModel( } viewModelScope.launch { - schoolDataSource.xapiStatementsResource.getAsFlow( + schoolDataSource.xapiResource.statements.getAsFlow( listParams = GetStatementParams( activity = CATEGORY_CLASS_MANAGEMENT, relatedActivities = true, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 2cf61adeb..8a4e5d8f9 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -140,7 +140,7 @@ class StudentGroupingDetailViewModel( ), timestamp = Clock.System.now() ) - schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + schoolDataSource.xapiResource.statements.post(listOf(voidingStatement)) _navCommandFlow.tryEmit(NavCommand.PopUp()) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt index 0dfb3d4fc..0cac3d8ec 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/edit/StudentGroupingEditViewModel.kt @@ -129,7 +129,7 @@ class StudentGroupingEditViewModel( @OptIn(ExperimentalUuidApi::class) viewModelScope.launch { try { - val statementResult = schoolDataSource.xapiStatementsResource.get( + val statementResult = schoolDataSource.xapiResource.statements.get( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, activity = classActivityId, @@ -228,7 +228,7 @@ class StudentGroupingEditViewModel( ), timestamp = Clock.System.now() ) - schoolDataSource.xapiStatementsResource.post(listOf(voidingStatement)) + schoolDataSource.xapiResource.statements.post(listOf(voidingStatement)) } val members = _uiState.value.selectedStudents.map { student -> @@ -275,7 +275,7 @@ class StudentGroupingEditViewModel( ) ) - schoolDataSource.xapiStatementsResource.post(listOf(statement)) + schoolDataSource.xapiResource.statements.post(listOf(statement)) if (route.groupId == null) { _navCommandFlow.tryEmit( From e2e79267a0a7aa5a7b4eac73ca00aaa9317d25cd Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 04:38:31 +0530 Subject: [PATCH 107/130] Update `schoolDataSource` access to use `xapiResource.statements` across various ViewModels. --- .../shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt | 2 +- .../respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt | 2 +- .../studentgrouping/detail/StudentGroupingDetailViewModel.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index f54603153..0d0c141f7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -209,7 +209,7 @@ class ClazzDetailViewModel( } viewModelScope.launch { - schoolDataSource.xapiStatementsResource.getAsFlow( + schoolDataSource.xapiResource.statements.getAsFlow( listParams = GetStatementParams( activity = route.classActivityId, ), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt index ff129ecd7..33b54f8aa 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/edit/ClazzEditViewModel.kt @@ -178,7 +178,7 @@ class ClazzEditViewModel( ?: throw IllegalStateException("onClickSave: statement data cannot be null after validation") launchWithLoadingIndicator { - schoolDataSource.xapiStatementsResource.post( + schoolDataSource.xapiResource.statements.post( listOf( classStatement.copy( id = Uuid.random(), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt index 8a4e5d8f9..0235699e7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/studentgrouping/detail/StudentGroupingDetailViewModel.kt @@ -158,7 +158,7 @@ class StudentGroupingDetailViewModel( ) ) - schoolDataSource.xapiStatementsResource.getAsFlow( + schoolDataSource.xapiResource.statements.getAsFlow( listParams = XapiStatementsResource.GetStatementParams( verb = VERB_SAVED, agent = groupAgent, From 33ad990ad1ce1219c2f72e355ee085cfb4dbc75b Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 05:10:46 +0530 Subject: [PATCH 108/130] Update `schoolDataSource` access to use `xapiResource.statements` across various ViewModels. --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/flows/{test => }/001_002_add_user_direct_test.yaml | 0 .../flows/{test => }/001_003_login_using_school_link_test.yaml | 0 .maestro/flows/{test => }/002_browse_lessons_test.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/flows/{test => }/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/flows/{test => }/001_002_add_user_direct_test.yaml (100%) rename .maestro/flows/{test => }/001_003_login_using_school_link_test.yaml (100%) rename .maestro/flows/{test => }/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows/test/001_002_add_user_direct_test.yaml b/.maestro/flows/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows/test/001_002_add_user_direct_test.yaml rename to .maestro/flows/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows/test/001_003_login_using_school_link_test.yaml b/.maestro/flows/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows/test/001_003_login_using_school_link_test.yaml rename to .maestro/flows/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows/test/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows/test/002_browse_lessons_test.yaml rename to .maestro/flows/002_browse_lessons_test.yaml From d966bce5fd1fc96630d6d67d792c988effcc9849 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 13:08:21 +0530 Subject: [PATCH 109/130] moving file just to test one single --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/flows/{ => test}/001_002_add_user_direct_test.yaml | 0 .../flows/{ => test}/001_003_login_using_school_link_test.yaml | 0 .maestro/flows/{ => test}/002_browse_lessons_test.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/flows/{ => test}/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/flows/{ => test}/001_002_add_user_direct_test.yaml (100%) rename .maestro/flows/{ => test}/001_003_login_using_school_link_test.yaml (100%) rename .maestro/flows/{ => test}/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows/001_002_add_user_direct_test.yaml b/.maestro/flows/test/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows/001_002_add_user_direct_test.yaml rename to .maestro/flows/test/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows/001_003_login_using_school_link_test.yaml b/.maestro/flows/test/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows/001_003_login_using_school_link_test.yaml rename to .maestro/flows/test/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/test/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows/002_browse_lessons_test.yaml rename to .maestro/flows/test/002_browse_lessons_test.yaml From 3fc1701fddc4771da16669b7da9805ca9ad304aa Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 13:37:46 +0530 Subject: [PATCH 110/130] Add conditional back navigation to admin assignment Maestro test flow. --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 48a2c331a..8a6b015bb 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -22,7 +22,11 @@ onFlowComplete: file: "subflows/admin_add_class_with_members.yaml" env: CLASSNAME: "TestClass" - +- runFlow: + when: + notVisible: "Classes" + commands: + - back - tapOn: "Classes" - tapOn: "TestClass" - scrollUntilVisible: From 63430bd308a29c2d48d0ef28d7a6e9e33634bf82 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 14:31:18 +0530 Subject: [PATCH 111/130] Update Maestro test flows with improved assertions and move test files to the flows directory. --- ...vite_users_using_qr_code_or_link_test.yaml | 0 .../001_002_add_user_direct_test.yaml | 0 .../001_003_login_using_school_link_test.yaml | 0 .../{test => }/002_browse_lessons_test.yaml | 0 ...er_assigns_assignment_to_a_class_test.yaml | 21 +++++++++++++++++++ 5 files changed, 21 insertions(+) rename .maestro/flows/{test => }/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/flows/{test => }/001_002_add_user_direct_test.yaml (100%) rename .maestro/flows/{test => }/001_003_login_using_school_link_test.yaml (100%) rename .maestro/flows/{test => }/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows/test/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows/test/001_002_add_user_direct_test.yaml b/.maestro/flows/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows/test/001_002_add_user_direct_test.yaml rename to .maestro/flows/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows/test/001_003_login_using_school_link_test.yaml b/.maestro/flows/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows/test/001_003_login_using_school_link_test.yaml rename to .maestro/flows/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows/test/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows/test/002_browse_lessons_test.yaml rename to .maestro/flows/002_browse_lessons_test.yaml diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 8a6b015bb..db7cec920 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -138,7 +138,14 @@ onFlowComplete: env: USERNAME: "studentauser" PASSWORD: "test123" +- assertVisible: "Apps" - tapOn: "Assignments" +- assertVisible: + id: "app_title" + text: "Assignments" +- extendedWaitUntil: + visible: "Homework 1" + timeout: 10000 - tapOn: "Homework 1" - assertVisible: id: "app_title" @@ -173,7 +180,14 @@ onFlowComplete: env: USERNAME: "studentbuser" PASSWORD: "test123" +- assertVisible: "Apps" - tapOn: "Assignments" +- assertVisible: + id: "app_title" + text: "Assignments" +- extendedWaitUntil: + visible: "Homework 1" + timeout: 10000 - tapOn: "Homework 1" - assertVisible: id: "app_title" @@ -199,7 +213,14 @@ onFlowComplete: env: USERNAME: "teacherauser" PASSWORD: "test123" +- assertVisible: "Apps" - tapOn: "Assignments" +- assertVisible: + id: "app_title" + text: "Assignments" +- extendedWaitUntil: + visible: "Homework 1" + timeout: 10000 - assertVisible: "Homework 1" - assertVisible: "TestGroup.*" - tapOn: "Homework 1" From cd9c39c2a65bf4e606ac5d572157bd09f7fa3a45 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Tue, 16 Jun 2026 14:53:10 +0530 Subject: [PATCH 112/130] Update Maestro test flows with improved assertions and move test files to the flows directory. --- .../flows/subflows/admin_add_student.yaml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .maestro/flows/subflows/admin_add_student.yaml diff --git a/.maestro/flows/subflows/admin_add_student.yaml b/.maestro/flows/subflows/admin_add_student.yaml new file mode 100644 index 000000000..1d8c4776c --- /dev/null +++ b/.maestro/flows/subflows/admin_add_student.yaml @@ -0,0 +1,66 @@ +appId: world.respect.app + +--- +- tapOn: "Classes" +- tapOn: "New Class" +# Admin copy invite code to add student +- tapOn: "Add Student" +- copyTextFrom: + id: "invite_code" + + +# Student sign-up via invite code to the class +- clearState: world.respect.app +- launchApp: + arguments: + respect_directory: ${output.SCHOOL_URL} + +- tapOn: "Get Started" + +- runFlow: + file: "get_started_select_school_by_name.yaml" + env: + SCHOOL_NAME: ${SCHOOL_NAME} +- tapOn: "I have an invite code" +- tapOn: "Invite code" +- pasteText +- tapOn: "Next" +- assertVisible: + id: "app_title" +- tapOn: "I’m a Student" +- assertVisible: + id: "app_title" + text: "Your profile" +- tapOn: "Your name*" +- inputText: "Student User" +- tapOn: "Gender*" +- tapOn: "Female" +- tapOn: "Your date of birth*" +- runScript: + file: "../scripts/setDate.js" +- inputText: ${output.pastYearDateC} +- tapOn: "Next" +- assertVisible: + id: "app_title" + text: "Create account" +- assertVisible: "studentuser" #username +- tapOn: "Next" +- tapOn: "Password*" +- inputText: "tests1" +- tapOn: "Sign-up" +- assertVisible: + id: "app_title" + text: "Waiting for approval" +- assertVisible: "Please wait" + +# Admin approve student's request to join the class +- clearState: world.respect.app +- runFlow: "school_admin_login_flow.yaml" +- tapOn: "Classes" +- assertVisible: + id: "app_title" + text: "Classes" +- tapOn: "New Class" +- assertVisible: "Pending requests.*" +- assertVisible: "Student User.*" +- tapOn: "Accept Invite" \ No newline at end of file From 4039a3132dc7cc3ef88cc29d105824236ac11d17 Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 16 Jun 2026 15:11:13 +0400 Subject: [PATCH 113/130] Update Maestro flow `003_admin_user_assigns_assignment_to_a_class_test.yaml` to include navigation to classes, remove redundant wait timers, and add assertions for student assignment results. --- ...ser_assigns_assignment_to_a_class_test.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index db7cec920..74bbb3f0a 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -139,13 +139,12 @@ onFlowComplete: USERNAME: "studentauser" PASSWORD: "test123" - assertVisible: "Apps" +- tapOn: "Classes" +- assertVisible: "TestClass" - tapOn: "Assignments" - assertVisible: id: "app_title" text: "Assignments" -- extendedWaitUntil: - visible: "Homework 1" - timeout: 10000 - tapOn: "Homework 1" - assertVisible: id: "app_title" @@ -181,13 +180,12 @@ onFlowComplete: USERNAME: "studentbuser" PASSWORD: "test123" - assertVisible: "Apps" +- tapOn: "Classes" +- assertVisible: "TestClass" - tapOn: "Assignments" - assertVisible: id: "app_title" text: "Assignments" -- extendedWaitUntil: - visible: "Homework 1" - timeout: 10000 - tapOn: "Homework 1" - assertVisible: id: "app_title" @@ -214,13 +212,12 @@ onFlowComplete: USERNAME: "teacherauser" PASSWORD: "test123" - assertVisible: "Apps" +- tapOn: "Classes" +- assertVisible: "TestClass" - tapOn: "Assignments" - assertVisible: id: "app_title" text: "Assignments" -- extendedWaitUntil: - visible: "Homework 1" - timeout: 10000 - assertVisible: "Homework 1" - assertVisible: "TestGroup.*" - tapOn: "Homework 1" @@ -239,6 +236,9 @@ onFlowComplete: - assertVisible: "Average" - assertVisible: "95%" - assertVisible: "90%" +- tapOn: "90%" #studentB +- assertVisible: "Assignment Result Statement Demo: StudentB User" +- assertVisible: "Assignment Result Statement Demo" # Verify edit assignment is working - tapOn: From b6c4abc044416e2a3d1adf6921f0a7513b0c34ae Mon Sep 17 00:00:00 2001 From: pooja Date: Thu, 18 Jun 2026 14:14:03 +0400 Subject: [PATCH 114/130] Update Maestro flow `003_admin_user_assigns_assignment_to_a_class_test.yaml` to remove redundant navigation to the Classes tab and assertions for "TestClass" during student verification steps. --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 74bbb3f0a..71f961fa6 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -139,8 +139,6 @@ onFlowComplete: USERNAME: "studentauser" PASSWORD: "test123" - assertVisible: "Apps" -- tapOn: "Classes" -- assertVisible: "TestClass" - tapOn: "Assignments" - assertVisible: id: "app_title" @@ -180,8 +178,6 @@ onFlowComplete: USERNAME: "studentbuser" PASSWORD: "test123" - assertVisible: "Apps" -- tapOn: "Classes" -- assertVisible: "TestClass" - tapOn: "Assignments" - assertVisible: id: "app_title" From 43e68ba79a110bd3d429c378f28cff1620cd405b Mon Sep 17 00:00:00 2001 From: pooja Date: Thu, 18 Jun 2026 14:16:49 +0400 Subject: [PATCH 115/130] Update Maestro flow `003_admin_user_assigns_assignment_to_a_class_test.yaml` to change the target class name from "TestGroup" to "TestClass". --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 71f961fa6..bad1cc21d 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -69,7 +69,7 @@ onFlowComplete: - tapOn: "Assignment title*" - inputText: "Homework 1" - tapOn: "Assign to" -- tapOn: "TestGroup" +- tapOn: "TestClass" - tapOn: "Description" - inputText: "Please complete this Homework on time." - runFlow: From 1e4eba602c2c3c935191592751f4b4a9099255d3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Thu, 18 Jun 2026 22:28:54 +0530 Subject: [PATCH 116/130] Switch from `XapiGroup` to `Clazz` for student group selection and update related logic. - Refactor `AssignmentEditScreen` and `AssignmentEditViewModel` to use `Clazz` options instead of `XapiGroup` for the assignee field. - Update `UpdateClazzStudentXapiGroupUseCase` to resolve class names via xAPI activity statements rather than direct class lookup. - Add debug logging to `ClazzDetailViewModel` and `UpdateClazzStudentXapiGroupUseCase` to track group member synchronization. - Map `Clazz` objects to xAPI groups using `studentsXapiGroup` extension when selecting an assignee. --- .../assignment/edit/AssignmentEditScreen.kt | 16 ++--- .../UpdateClazzStudentXapiGroupUseCase.kt | 58 ++++++++++++++----- .../edit/AssignmentEditViewModel.kt | 57 ++++++------------ .../clazz/detail/ClazzDetailViewModel.kt | 5 ++ 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index 492fe0b91..e5e6e3d4b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -44,7 +44,7 @@ import world.respect.app.components.LangMapTextField import world.respect.app.components.RespectLocalDateTimeField import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource -import world.respect.lib.xapi.model.XapiGroup +import world.respect.datalayer.school.model.Clazz import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.xapi.ext.copyWithObjectActivityDescription import world.respect.lib.xapi.ext.copyWithObjectActivityName @@ -80,7 +80,7 @@ fun AssignmentEditScreen( onEntityChanged = viewModel::onEntityChanged, onAssigneeTextChanged = viewModel::onAssigneeTextChanged, onClickAddLearningUnit = viewModel::onClickAddLearningUnit, - onAssigneeGroupSelected = viewModel::onAssigneeGroupSelected, + onAssigneeClassSelected = viewModel::onAssigneeClassSelected, onClickRemoveLearningUnit = viewModel::onClickRemoveLearningUnit, ) } @@ -91,7 +91,7 @@ fun AssignmentEditScreen( uiState: AssignmentEditUiState, onEntityChanged: (XapiStatement) -> Unit, onAssigneeTextChanged: (String) -> Unit, - onAssigneeGroupSelected: (XapiGroup) -> Unit, + onAssigneeClassSelected: (Clazz) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (XapiActivity) -> Unit, ) { @@ -143,9 +143,9 @@ fun AssignmentEditScreen( }, colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), supportingText = { - Text(uiTextStringResource(uiState.groupError ?: Res.string.required.asUiText())) + Text(uiTextStringResource(uiState.classError ?: Res.string.required.asUiText())) }, - isError = uiState.groupError != null, + isError = uiState.classError != null, ) ExposedDropdownMenu( @@ -153,14 +153,14 @@ fun AssignmentEditScreen( expanded = expanded, onDismissRequest = { expanded = false } ) { - uiState.groupOptions.forEach { group -> + uiState.classOptions.forEach { clazz -> DropdownMenuItem( text = { - Text(group.name ?: "") + Text(clazz.title) }, onClick = { expanded = false - onAssigneeGroupSelected(group) + onAssigneeClassSelected(clazz) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt index 166d54aae..2f1699c42 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt @@ -1,6 +1,5 @@ package world.respect.shared.domain.enrollments -import io.github.aakira.napier.Napier import io.ktor.http.Url import world.respect.datalayer.AuthenticatedUserPrincipalId import world.respect.datalayer.SchoolDataSource @@ -9,9 +8,14 @@ import world.respect.datalayer.school.ext.asXapiAgent import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.ext.dataOrNull +import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull +import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiGroup +import world.respect.lib.xapi.model.XapiObjectType import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.model.XapiVerb -import world.respect.shared.ext.studentsXapiGroup +import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams import kotlin.uuid.ExperimentalUuidApi class UpdateClazzStudentXapiGroupUseCase( @@ -32,31 +36,59 @@ class UpdateClazzStudentXapiGroupUseCase( ) ).dataOrNull() - val clazz = schoolDataSource.classDataSource.findByGuid( - params = DataLoadParams(), - guid = clazzUid, - ).dataOrNull() + val classStatement = schoolDataSource.xapiResource.statements.get( + listParams = GetStatementParams(activity = clazzUid), + dataLoadParams = DataLoadParams(), + ).dataOrNull()?.statements?.mostRecentByTimestampOrNull() + + val classActivity = classStatement?.`object` as? XapiActivity + val className = classActivity?.definition?.name?.values?.firstOrNull() + + val studentsXapiGroup = if (classActivity != null) { + XapiGroup( + name = "${className ?: ""} students", + account = XapiAccount( + homePage = classActivity.id, + name = "students" + ), + objectType = XapiObjectType.Group, + ) + } else { + null + } val activePerson = schoolDataSource.personDataSource.findByGuid( loadParams = DataLoadParams(onlyIfCached = true), guid = authenticatedUserPrincipalId.guid, ).dataOrNull() - if(studentsInClass == null || clazz == null || activePerson == null) { - Napier.w("No enrollments: something bad: students=$studentsInClass clazz=$clazz") + println("UpdateClazzStudentXapiGroup: clazzUid='$clazzUid'") + println("UpdateClazzStudentXapiGroup: studentsInClass=${studentsInClass?.map { "${it.givenName} ${it.familyName}" }}") + println("UpdateClazzStudentXapiGroup: classActivity=${classActivity?.id}") + println("UpdateClazzStudentXapiGroup: className=$className") + println("UpdateClazzStudentXapiGroup: studentsXapiGroup=$studentsXapiGroup") + println("UpdateClazzStudentXapiGroup: activePerson=${activePerson?.let { "${it.givenName} ${it.familyName}" }}") + + if(studentsInClass == null || studentsXapiGroup == null || activePerson == null) { + println("UpdateClazzStudentXapiGroup: RETURNING EARLY - studentsInClass=${studentsInClass != null}, studentsXapiGroup=${studentsXapiGroup != null}, activePerson=${activePerson != null}") return } + val classDisplayName = className ?: "Unknown Class" + val studentNames = studentsInClass.map { "${it.givenName} ${it.familyName}" } + println("UpdateClazzStudentXapiGroup: Class='$classDisplayName', Students=$studentNames") + + val groupToPost = studentsXapiGroup.copy( + member = studentsInClass.map { it.asXapiAgent(schoolUrl) } + ) + println("UpdateClazzStudentXapiGroup: Posting XapiGroup name='${groupToPost.name}', account=${groupToPost.account}, members=${groupToPost.member?.map { it.name }}") + schoolDataSource.xapiResource.statements.post( listOf( XapiStatement( actor = activePerson.asXapiAgent(schoolUrl), verb = XapiVerb(id = XapiVerb.ID_SAVED), - `object` = clazz.studentsXapiGroup( - schoolUrl = schoolUrl - ).copy( - member = studentsInClass.map { it.asXapiAgent(schoolUrl) } - ) + `object` = groupToPost ) ) ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 5ada69881..1b1bd509f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -17,9 +17,9 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.school.ClassDataSource +import world.respect.datalayer.school.model.Clazz import world.respect.lib.dataloadstate.DataLoadParams -import world.respect.lib.xapi.model.VERB_SAVED -import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.dataloadstate.DataLoadState import world.respect.lib.dataloadstate.DataLoadingState import world.respect.lib.dataloadstate.DataReadyState @@ -42,8 +42,8 @@ import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.opds.getxapiactivityid.GetXapiActivityForPublicationUseCase import world.respect.shared.domain.xapi.createBlankAssignmentStatement +import world.respect.shared.ext.studentsXapiGroup import world.respect.shared.generated.resources.Res -import kotlin.time.Instant import world.respect.shared.generated.resources.add_assignment import world.respect.shared.generated.resources.edit_assignment import world.respect.shared.generated.resources.required_field @@ -70,15 +70,15 @@ data class AssignmentEditUiState( val statementData: DataLoadState = DataLoadingState(), val assignee: String = "", val nameError: UiText? = null, - val groupOptions: List = emptyList(), - val groupError: UiText? = null, + val classOptions: List = emptyList(), + val classError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, ) { val fieldsEnabled: Boolean get() = statementData.isReadyAndSettled() val hasErrors: Boolean - get() = nameError != null || groupError != null + get() = nameError != null || classError != null } @OptIn(ExperimentalUuidApi::class) @@ -135,32 +135,14 @@ class AssignmentEditViewModel( launchWithLoadingIndicator( onShowError = { snackBarDispatcher.showSnackBar(Snack(it)) } ) { - val statementResult = schoolDataSource.xapiResource.statements.get( - listParams = GetStatementParams( - verb = VERB_SAVED, - ), - dataLoadParams = DataLoadParams() - ).dataOrNull() - - val allGroups = statementResult?.statements - ?.filter { it.verb.id == VERB_SAVED } - ?.sortedByDescending { it.timestamp ?: it.stored ?: Instant.DISTANT_PAST } - ?.mapNotNull { it.`object` as? XapiGroup } - ?.filter { group -> - group.account?.name != null && - group.account?.homePage == schoolUrl.toString() - } - ?.distinctBy { it.account?.name } - ?.map { group -> - val filteredMembers = group.member?.filter { agent -> - agent.name != null - } - group.copy(member = filteredMembers) - } ?: emptyList() + val classes = schoolDataSource.classDataSource.list( + DataLoadParams(), + ClassDataSource.GetListParams() + ).dataOrNull() ?: emptyList() _uiState.update { it.copy( - groupOptions = allGroups, + classOptions = classes, ) } @@ -236,20 +218,15 @@ class AssignmentEditViewModel( ) } - fun onAssigneeGroupSelected(group: XapiGroup) { + fun onAssigneeClassSelected(clazz: Clazz) { val statement = _uiState.value.statementData.dataOrNull() ?: return _uiState.update { it.copy( statementData = DataReadyState( - statement.copy( - actor = XapiGroup( - name = group.name, - account = group.account, - ) - ) + statement.copy(actor = clazz.studentsXapiGroup(schoolUrl)) ), - assignee = group.name ?: "", - groupError = null, + assignee = clazz.title, + classError = null, ) } } @@ -272,7 +249,7 @@ class AssignmentEditViewModel( fun onAssigneeTextChanged(text: String) { _uiState.update { - it.copy(assignee = text, groupError = null) + it.copy(assignee = text, classError = null) } } @@ -314,7 +291,7 @@ class AssignmentEditViewModel( nameError = Res.string.required_field.asUiText().takeIf { statement?.objectActivityOrNull()?.definition?.name.isNullOrAllBlank() }, - groupError = Res.string.required_field.asUiText().takeIf { + classError = Res.string.required_field.asUiText().takeIf { statement?.actor?.name.isNullOrEmpty() } ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 0d0c141f7..113f1e4fc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -502,6 +502,11 @@ class ClazzDetailViewModel( group.copy(member = filteredMembers) } + println("observeGroupsFromXapi: latestGroups count=${latestGroups.size}") + latestGroups.forEach { group -> + println("observeGroupsFromXapi: Group name='${group.name}', account=${group.account}, members=${group.member?.map { it.name }}") + } + _uiState.update { prev -> prev.copy(groups = latestGroups) } From 65809dc28bb29b959c5ded3a9cb82ebde1313bca Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 01:34:51 +0530 Subject: [PATCH 117/130] Refactor `AssignmentEditViewModel` to use `XapiGroup` for class selection and remove debug logs in `ClazzDetailViewModel`. --- .../assignment/edit/AssignmentEditScreen.kt | 16 ++++++--- .../edit/AssignmentEditViewModel.kt | 35 ++++++++++++------- .../clazz/detail/ClazzDetailViewModel.kt | 5 --- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index e5e6e3d4b..aaa459a02 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -44,7 +44,7 @@ import world.respect.app.components.LangMapTextField import world.respect.app.components.RespectLocalDateTimeField import world.respect.app.components.defaultItemPadding import world.respect.app.components.uiTextStringResource -import world.respect.datalayer.school.model.Clazz +import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.xapi.ext.copyWithObjectActivityDescription import world.respect.lib.xapi.ext.copyWithObjectActivityName @@ -91,7 +91,7 @@ fun AssignmentEditScreen( uiState: AssignmentEditUiState, onEntityChanged: (XapiStatement) -> Unit, onAssigneeTextChanged: (String) -> Unit, - onAssigneeClassSelected: (Clazz) -> Unit, + onAssigneeClassSelected: (XapiGroup) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (XapiActivity) -> Unit, ) { @@ -118,6 +118,11 @@ fun AssignmentEditScreen( var expanded by remember { mutableStateOf(false) } + println("AssignmentEditScreen: classOptions count=${uiState.classOptions.size}") + uiState.classOptions.forEach { group -> + println("AssignmentEditScreen: Group name='${group.name}', members=${group.member?.map { it.name }}") + } + //As per https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1) ExposedDropdownMenuBox( expanded = expanded, @@ -153,14 +158,15 @@ fun AssignmentEditScreen( expanded = expanded, onDismissRequest = { expanded = false } ) { - uiState.classOptions.forEach { clazz -> + uiState.classOptions.forEach { group -> DropdownMenuItem( text = { - Text(clazz.title) + Text(group.name ?: "") }, onClick = { expanded = false - onAssigneeClassSelected(clazz) + println("AssignmentEditScreen: Selected group='${group.name}', assigning to members=${group.member?.map { it.name }}") + onAssigneeClassSelected(group) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 1b1bd509f..12f8019d5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -17,8 +17,6 @@ import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.school.ClassDataSource -import world.respect.datalayer.school.model.Clazz import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.DataLoadState import world.respect.lib.dataloadstate.DataLoadingState @@ -34,6 +32,7 @@ import world.respect.lib.xapi.ext.objectActivityNameOrNull import world.respect.lib.xapi.ext.objectActivityOrNull import world.respect.lib.xapi.ext.removeActivityFromContextActivitiesGrouping import world.respect.lib.xapi.model.XapiActivity +import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams @@ -42,7 +41,6 @@ import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.opds.getxapiactivityid.GetXapiActivityForPublicationUseCase import world.respect.shared.domain.xapi.createBlankAssignmentStatement -import world.respect.shared.ext.studentsXapiGroup import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_assignment import world.respect.shared.generated.resources.edit_assignment @@ -70,7 +68,7 @@ data class AssignmentEditUiState( val statementData: DataLoadState = DataLoadingState(), val assignee: String = "", val nameError: UiText? = null, - val classOptions: List = emptyList(), + val classOptions: List = emptyList(), val classError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, ) { @@ -135,14 +133,26 @@ class AssignmentEditViewModel( launchWithLoadingIndicator( onShowError = { snackBarDispatcher.showSnackBar(Snack(it)) } ) { - val classes = schoolDataSource.classDataSource.list( - DataLoadParams(), - ClassDataSource.GetListParams() - ).dataOrNull() ?: emptyList() + val savedStatements = schoolDataSource.xapiResource.statements.get( + listParams = GetStatementParams( + verb = XapiVerb.ID_SAVED, + ), + dataLoadParams = DataLoadParams(), + ).dataOrNull()?.statements ?: emptyList() + + val studentGroups = savedStatements + .mapNotNull { statement -> + val group = statement.`object` as? XapiGroup + group?.takeIf { it.account?.name == "students" } + } + .sortedByDescending { it.member?.size ?: 0 } + .distinctBy { it.account?.homePage } + + println("AssignmentEdit: studentGroups=${studentGroups.map { "${it.name} (members=${it.member?.map { m -> m.name }})" }}") _uiState.update { it.copy( - classOptions = classes, + classOptions = studentGroups, ) } @@ -218,14 +228,15 @@ class AssignmentEditViewModel( ) } - fun onAssigneeClassSelected(clazz: Clazz) { + fun onAssigneeClassSelected(group: XapiGroup) { val statement = _uiState.value.statementData.dataOrNull() ?: return + println("AssignmentEdit: onAssigneeClassSelected group='${group.name}', members=${group.member?.map { it.name }}") _uiState.update { it.copy( statementData = DataReadyState( - statement.copy(actor = clazz.studentsXapiGroup(schoolUrl)) + statement.copy(actor = group) ), - assignee = clazz.title, + assignee = group.name ?: "", classError = null, ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt index 113f1e4fc..0d0c141f7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/clazz/detail/ClazzDetailViewModel.kt @@ -502,11 +502,6 @@ class ClazzDetailViewModel( group.copy(member = filteredMembers) } - println("observeGroupsFromXapi: latestGroups count=${latestGroups.size}") - latestGroups.forEach { group -> - println("observeGroupsFromXapi: Group name='${group.name}', account=${group.account}, members=${group.member?.map { it.name }}") - } - _uiState.update { prev -> prev.copy(groups = latestGroups) } From 9bd4dc428f2ceb73e415960f8bd0124fcbf89532 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 01:37:33 +0530 Subject: [PATCH 118/130] testing. --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/{flows => test}/001_002_add_user_direct_test.yaml | 0 .../{flows => test}/001_003_login_using_school_link_test.yaml | 0 .maestro/{flows => test}/002_browse_lessons_test.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/{flows => test}/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/{flows => test}/001_002_add_user_direct_test.yaml (100%) rename .maestro/{flows => test}/001_003_login_using_school_link_test.yaml (100%) rename .maestro/{flows => test}/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/test/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/test/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/flows/001_002_add_user_direct_test.yaml b/.maestro/test/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/flows/001_002_add_user_direct_test.yaml rename to .maestro/test/001_002_add_user_direct_test.yaml diff --git a/.maestro/flows/001_003_login_using_school_link_test.yaml b/.maestro/test/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/flows/001_003_login_using_school_link_test.yaml rename to .maestro/test/001_003_login_using_school_link_test.yaml diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/test/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/flows/002_browse_lessons_test.yaml rename to .maestro/test/002_browse_lessons_test.yaml From d70bc73f2e885abb3c3365d5e8a7a0fb78a63e95 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 01:54:15 +0530 Subject: [PATCH 119/130] Remove group creation steps and update class selection in admin assignment test flow. --- ...er_assigns_assignment_to_a_class_test.yaml | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index bad1cc21d..579ad7394 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -22,28 +22,6 @@ onFlowComplete: file: "subflows/admin_add_class_with_members.yaml" env: CLASSNAME: "TestClass" -- runFlow: - when: - notVisible: "Classes" - commands: - - back -- tapOn: "Classes" -- tapOn: "TestClass" -- scrollUntilVisible: - element: "Create group" - direction: DOWN -- tapOn: "Create group" -- assertVisible: - id: "app_title" - text: "Create group" -- tapOn: - id: "name" -- inputText: "TestGroup" -- hideKeyboard -- tapOn: "StudentA User" -- tapOn: "StudentB User" -- tapOn: "StudentC User" -- tapOn: "Save" # Teacher Add Assignment - clearState: world.respect.app @@ -69,7 +47,7 @@ onFlowComplete: - tapOn: "Assignment title*" - inputText: "Homework 1" - tapOn: "Assign to" -- tapOn: "TestClass" +- tapOn: "TestClass students" - tapOn: "Description" - inputText: "Please complete this Homework on time." - runFlow: From 6d791d369e7327515a5c2cbaf870c74d2d8d6865 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 01:56:27 +0530 Subject: [PATCH 120/130] Remove group creation steps and update class selection in admin assignment test flow. --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 579ad7394..67ca95eec 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -187,7 +187,7 @@ onFlowComplete: PASSWORD: "test123" - assertVisible: "Apps" - tapOn: "Classes" -- assertVisible: "TestClass" +- assertVisible: "TestClass students" - tapOn: "Assignments" - assertVisible: id: "app_title" From a3fb3589b06cc2879f112617979b2ab8b9542361 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 02:09:52 +0530 Subject: [PATCH 121/130] Update class visibility assertion in admin assignment Maestro test flow. --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 67ca95eec..79e53d814 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -97,7 +97,7 @@ onFlowComplete: - assertVisible: "In-progress (0)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" -- assertVisible: "TestGroup.*" +- assertVisible: "TestClass students" - assertVisible: "StudentA User" - assertVisible: "StudentB User" - assertVisible: "StudentC User" @@ -193,7 +193,7 @@ onFlowComplete: id: "app_title" text: "Assignments" - assertVisible: "Homework 1" -- assertVisible: "TestGroup.*" +- assertVisible: "TestClass students" - tapOn: "Homework 1" - assertVisible: "All (3)" - assertVisible: "Completed (1)" @@ -202,7 +202,7 @@ onFlowComplete: #- assertVisible: "Not started (1)" - assertVisible: "Please complete this Homework on time." - assertVisible: "Assigned to" -- assertVisible: "TestGroup.*" +- assertVisible: "TestClass students" - assertVisible: "StudentA User" - assertVisible: "StudentB User" - assertVisible: "StudentC User" From 743960081f7a0065927bdd46208bc2ada8ea0f3c Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 02:28:53 +0530 Subject: [PATCH 122/130] Update class visibility assertion in admin assignment Maestro test flow. --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 79e53d814..89ebc399e 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -187,7 +187,7 @@ onFlowComplete: PASSWORD: "test123" - assertVisible: "Apps" - tapOn: "Classes" -- assertVisible: "TestClass students" +- assertVisible: "TestClass" - tapOn: "Assignments" - assertVisible: id: "app_title" From 9a6dc7989e1fc463aaf6046641cd9923ef480ea6 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 02:49:53 +0530 Subject: [PATCH 123/130] Remove debug print statements and update Maestro test flow. --- ...admin_user_assigns_assignment_to_a_class_test.yaml | 3 --- .../app/view/assignment/edit/AssignmentEditScreen.kt | 6 ------ .../enrollments/UpdateClazzStudentXapiGroupUseCase.kt | 11 ----------- .../assignment/edit/AssignmentEditViewModel.kt | 3 --- 4 files changed, 23 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 89ebc399e..6a97b1a83 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -210,9 +210,6 @@ onFlowComplete: - assertVisible: "Average" - assertVisible: "95%" - assertVisible: "90%" -- tapOn: "90%" #studentB -- assertVisible: "Assignment Result Statement Demo: StudentB User" -- assertVisible: "Assignment Result Statement Demo" # Verify edit assignment is working - tapOn: diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index aaa459a02..add6e4453 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -118,11 +118,6 @@ fun AssignmentEditScreen( var expanded by remember { mutableStateOf(false) } - println("AssignmentEditScreen: classOptions count=${uiState.classOptions.size}") - uiState.classOptions.forEach { group -> - println("AssignmentEditScreen: Group name='${group.name}', members=${group.member?.map { it.name }}") - } - //As per https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1) ExposedDropdownMenuBox( expanded = expanded, @@ -165,7 +160,6 @@ fun AssignmentEditScreen( }, onClick = { expanded = false - println("AssignmentEditScreen: Selected group='${group.name}', assigning to members=${group.member?.map { it.name }}") onAssigneeClassSelected(group) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt index 2f1699c42..6458556bb 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt @@ -62,26 +62,15 @@ class UpdateClazzStudentXapiGroupUseCase( guid = authenticatedUserPrincipalId.guid, ).dataOrNull() - println("UpdateClazzStudentXapiGroup: clazzUid='$clazzUid'") - println("UpdateClazzStudentXapiGroup: studentsInClass=${studentsInClass?.map { "${it.givenName} ${it.familyName}" }}") - println("UpdateClazzStudentXapiGroup: classActivity=${classActivity?.id}") - println("UpdateClazzStudentXapiGroup: className=$className") - println("UpdateClazzStudentXapiGroup: studentsXapiGroup=$studentsXapiGroup") - println("UpdateClazzStudentXapiGroup: activePerson=${activePerson?.let { "${it.givenName} ${it.familyName}" }}") if(studentsInClass == null || studentsXapiGroup == null || activePerson == null) { println("UpdateClazzStudentXapiGroup: RETURNING EARLY - studentsInClass=${studentsInClass != null}, studentsXapiGroup=${studentsXapiGroup != null}, activePerson=${activePerson != null}") return } - val classDisplayName = className ?: "Unknown Class" - val studentNames = studentsInClass.map { "${it.givenName} ${it.familyName}" } - println("UpdateClazzStudentXapiGroup: Class='$classDisplayName', Students=$studentNames") - val groupToPost = studentsXapiGroup.copy( member = studentsInClass.map { it.asXapiAgent(schoolUrl) } ) - println("UpdateClazzStudentXapiGroup: Posting XapiGroup name='${groupToPost.name}', account=${groupToPost.account}, members=${groupToPost.member?.map { it.name }}") schoolDataSource.xapiResource.statements.post( listOf( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 12f8019d5..86d7a8bdc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -148,8 +148,6 @@ class AssignmentEditViewModel( .sortedByDescending { it.member?.size ?: 0 } .distinctBy { it.account?.homePage } - println("AssignmentEdit: studentGroups=${studentGroups.map { "${it.name} (members=${it.member?.map { m -> m.name }})" }}") - _uiState.update { it.copy( classOptions = studentGroups, @@ -230,7 +228,6 @@ class AssignmentEditViewModel( fun onAssigneeClassSelected(group: XapiGroup) { val statement = _uiState.value.statementData.dataOrNull() ?: return - println("AssignmentEdit: onAssigneeClassSelected group='${group.name}', members=${group.member?.map { it.name }}") _uiState.update { it.copy( statementData = DataReadyState( From ff9d552d87656dd823c0ddd34cd978e1fe18e47c Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 03:07:41 +0530 Subject: [PATCH 124/130] Move Maestro test files from `.maestro/test` to `.maestro/flows`. --- .../001_001_invite_users_using_qr_code_or_link_test.yaml | 0 .maestro/{test => flows}/001_002_add_user_direct_test.yaml | 0 .../{test => flows}/001_003_login_using_school_link_test.yaml | 0 .maestro/{test => flows}/002_browse_lessons_test.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename .maestro/{test => flows}/001_001_invite_users_using_qr_code_or_link_test.yaml (100%) rename .maestro/{test => flows}/001_002_add_user_direct_test.yaml (100%) rename .maestro/{test => flows}/001_003_login_using_school_link_test.yaml (100%) rename .maestro/{test => flows}/002_browse_lessons_test.yaml (100%) diff --git a/.maestro/test/001_001_invite_users_using_qr_code_or_link_test.yaml b/.maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml similarity index 100% rename from .maestro/test/001_001_invite_users_using_qr_code_or_link_test.yaml rename to .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml diff --git a/.maestro/test/001_002_add_user_direct_test.yaml b/.maestro/flows/001_002_add_user_direct_test.yaml similarity index 100% rename from .maestro/test/001_002_add_user_direct_test.yaml rename to .maestro/flows/001_002_add_user_direct_test.yaml diff --git a/.maestro/test/001_003_login_using_school_link_test.yaml b/.maestro/flows/001_003_login_using_school_link_test.yaml similarity index 100% rename from .maestro/test/001_003_login_using_school_link_test.yaml rename to .maestro/flows/001_003_login_using_school_link_test.yaml diff --git a/.maestro/test/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml similarity index 100% rename from .maestro/test/002_browse_lessons_test.yaml rename to .maestro/flows/002_browse_lessons_test.yaml From 4cf805e32bbc723fdaeed822b00efed54c7570b1 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 03:26:30 +0530 Subject: [PATCH 125/130] Refine error handling and student group filtering in `AssignmentEditViewModel`. --- .../edit/AssignmentEditViewModel.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 86d7a8bdc..5b2bdc4f2 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -3,6 +3,7 @@ package world.respect.shared.viewmodel.assignment.edit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import io.github.aakira.napier.Napier import io.ktor.http.Url import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -145,6 +146,12 @@ class AssignmentEditViewModel( val group = statement.`object` as? XapiGroup group?.takeIf { it.account?.name == "students" } } + .filter { group -> + if(group.name == null) { + Napier.w("AssignmentEditViewModel: student group has no name (homePage=${group.account?.homePage})") + } + group.name != null + } .sortedByDescending { it.member?.size ?: 0 } .distinctBy { it.account?.homePage } @@ -182,7 +189,7 @@ class AssignmentEditViewModel( ) } else { val instructor = accountManager.selectedAccountAndPersonFlow.first()?.xapiAgent - ?: return@launchWithLoadingIndicator + ?: throw IllegalStateException("Cannot create assignment: no active account xapiAgent found") val baseStmt = createBlankAssignmentStatement( assignmentActivityId = assignmentActivityId, instructor = instructor @@ -205,7 +212,8 @@ class AssignmentEditViewModel( val activity = getXapiActivityForPublicationUseCase(learningUnit.selectedPublication) _uiState.update { prev -> - val preStatementData = prev.statementData.dataOrNull() ?: return@update prev + val preStatementData = prev.statementData.dataOrNull() + ?: throw IllegalStateException("Learning unit result received but statement data is null") prev.copy( statementData = DataReadyState( @@ -227,13 +235,16 @@ class AssignmentEditViewModel( } fun onAssigneeClassSelected(group: XapiGroup) { - val statement = _uiState.value.statementData.dataOrNull() ?: return + val statement = _uiState.value.statementData.dataOrNull() + ?: throw IllegalStateException("onAssigneeClassSelected: statement data cannot be null") + val groupName = group.name + ?: throw IllegalStateException("onAssigneeClassSelected: group name cannot be null") _uiState.update { it.copy( statementData = DataReadyState( statement.copy(actor = group) ), - assignee = group.name ?: "", + assignee = groupName, classError = null, ) } @@ -278,7 +289,8 @@ class AssignmentEditViewModel( fun onClickRemoveLearningUnit( activity: XapiActivity ) { - val assignment = uiState.value.statementData.dataOrNull() ?: return + val assignment = uiState.value.statementData.dataOrNull() + ?: throw IllegalStateException("onClickRemoveLearningUnit: statement data cannot be null") _uiState.update { prev -> prev.copy( @@ -308,7 +320,8 @@ class AssignmentEditViewModel( if (stateToSave.hasErrors) return - val assignment = uiState.value.statementData.dataOrNull() ?: return + val assignment = uiState.value.statementData.dataOrNull() + ?: throw IllegalStateException("onClickSave: statement data cannot be null after validation") launchWithLoadingIndicator { schoolDataSource.xapiResource.statements.post( From d0885d06ad84ca0f6afd258b69c5d70617a791a4 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 03:32:27 +0530 Subject: [PATCH 126/130] Refactor `studentsXapiGroup` extension to target `XapiActivity` and use it in `UpdateClazzStudentXapiGroupUseCase`. --- .../UpdateClazzStudentXapiGroupUseCase.kt | 25 ++++--------------- .../world/respect/shared/ext/ClazzExt.kt | 25 ++++++++----------- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt index 6458556bb..f52b03257 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt @@ -9,13 +9,11 @@ import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.lib.dataloadstate.DataLoadParams import world.respect.lib.dataloadstate.ext.dataOrNull import world.respect.lib.xapi.ext.mostRecentByTimestampOrNull -import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity -import world.respect.lib.xapi.model.XapiGroup -import world.respect.lib.xapi.model.XapiObjectType import world.respect.lib.xapi.model.XapiStatement import world.respect.lib.xapi.model.XapiVerb import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParams +import world.respect.shared.ext.studentsXapiGroup import kotlin.uuid.ExperimentalUuidApi class UpdateClazzStudentXapiGroupUseCase( @@ -42,20 +40,8 @@ class UpdateClazzStudentXapiGroupUseCase( ).dataOrNull()?.statements?.mostRecentByTimestampOrNull() val classActivity = classStatement?.`object` as? XapiActivity - val className = classActivity?.definition?.name?.values?.firstOrNull() - val studentsXapiGroup = if (classActivity != null) { - XapiGroup( - name = "${className ?: ""} students", - account = XapiAccount( - homePage = classActivity.id, - name = "students" - ), - objectType = XapiObjectType.Group, - ) - } else { - null - } + val studentsXapiGroup = classActivity?.studentsXapiGroup() val activePerson = schoolDataSource.personDataSource.findByGuid( loadParams = DataLoadParams(onlyIfCached = true), @@ -63,12 +49,12 @@ class UpdateClazzStudentXapiGroupUseCase( ).dataOrNull() - if(studentsInClass == null || studentsXapiGroup == null || activePerson == null) { + if (studentsInClass == null || studentsXapiGroup == null || activePerson == null) { println("UpdateClazzStudentXapiGroup: RETURNING EARLY - studentsInClass=${studentsInClass != null}, studentsXapiGroup=${studentsXapiGroup != null}, activePerson=${activePerson != null}") return } - val groupToPost = studentsXapiGroup.copy( + val studentsGroup = studentsXapiGroup.copy( member = studentsInClass.map { it.asXapiAgent(schoolUrl) } ) @@ -77,10 +63,9 @@ class UpdateClazzStudentXapiGroupUseCase( XapiStatement( actor = activePerson.asXapiAgent(schoolUrl), verb = XapiVerb(id = XapiVerb.ID_SAVED), - `object` = groupToPost + `object` = studentsGroup ) ) ) } - } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt index ac42f5aee..e8291a020 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt @@ -1,29 +1,24 @@ package world.respect.shared.ext -import io.ktor.http.Url -import world.respect.datalayer.school.model.Clazz import world.respect.lib.xapi.model.XapiAccount +import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.model.XapiObjectType -import world.respect.libutil.ext.appendEndpointSegments -fun Clazz.activityId( - schoolUrl: Url -): String { - return schoolUrl.appendEndpointSegments("classes", guid).toString() -} -fun Clazz.studentsXapiGroup( - schoolUrl: Url -): XapiGroup { +/** + * Creates a students XapiGroup from this XapiActivity representing a class. + * The group name is derived from the activity definition name (first available value). + */ +fun XapiActivity.studentsXapiGroup(): XapiGroup { + val className = definition?.name?.values?.firstOrNull() return XapiGroup( - //This needs localized: however we don't currently have localization wrappers for formatted strings - - name = "$title students", + name = "${className ?: ""} students", account = XapiAccount( - homePage = activityId(schoolUrl), + homePage = id, name = "students" ), objectType = XapiObjectType.Group, ) } + From e7a7b6507ace23cd8413d012cc2c335b5d8ee2a4 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 03:37:47 +0530 Subject: [PATCH 127/130] Refactor `studentsXapiGroup` extension to target `XapiActivity` and use it in `UpdateClazzStudentXapiGroupUseCase`. --- .../UpdateClazzStudentXapiGroupUseCase.kt | 3 ++- .../assignment/edit/AssignmentEditViewModel.kt | 15 +++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt index f52b03257..3ebe561e5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt @@ -1,5 +1,6 @@ package world.respect.shared.domain.enrollments +import io.github.aakira.napier.Napier import io.ktor.http.Url import world.respect.datalayer.AuthenticatedUserPrincipalId import world.respect.datalayer.SchoolDataSource @@ -50,7 +51,7 @@ class UpdateClazzStudentXapiGroupUseCase( if (studentsInClass == null || studentsXapiGroup == null || activePerson == null) { - println("UpdateClazzStudentXapiGroup: RETURNING EARLY - studentsInClass=${studentsInClass != null}, studentsXapiGroup=${studentsXapiGroup != null}, activePerson=${activePerson != null}") + Napier.w("No enrollments: something bad: students=$studentsInClass clazz=$classStatement") return } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 5b2bdc4f2..e75ab3f7e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -144,7 +144,7 @@ class AssignmentEditViewModel( val studentGroups = savedStatements .mapNotNull { statement -> val group = statement.`object` as? XapiGroup - group?.takeIf { it.account?.name == "students" } + group?.takeIf { it.account?.name == STUDENTS } } .filter { group -> if(group.name == null) { @@ -189,7 +189,7 @@ class AssignmentEditViewModel( ) } else { val instructor = accountManager.selectedAccountAndPersonFlow.first()?.xapiAgent - ?: throw IllegalStateException("Cannot create assignment: no active account xapiAgent found") + ?: return@launchWithLoadingIndicator val baseStmt = createBlankAssignmentStatement( assignmentActivityId = assignmentActivityId, instructor = instructor @@ -212,8 +212,7 @@ class AssignmentEditViewModel( val activity = getXapiActivityForPublicationUseCase(learningUnit.selectedPublication) _uiState.update { prev -> - val preStatementData = prev.statementData.dataOrNull() - ?: throw IllegalStateException("Learning unit result received but statement data is null") + val preStatementData = prev.statementData.dataOrNull() ?: return@update prev prev.copy( statementData = DataReadyState( @@ -289,8 +288,7 @@ class AssignmentEditViewModel( fun onClickRemoveLearningUnit( activity: XapiActivity ) { - val assignment = uiState.value.statementData.dataOrNull() - ?: throw IllegalStateException("onClickRemoveLearningUnit: statement data cannot be null") + val assignment = uiState.value.statementData.dataOrNull() ?: return _uiState.update { prev -> prev.copy( @@ -320,8 +318,7 @@ class AssignmentEditViewModel( if (stateToSave.hasErrors) return - val assignment = uiState.value.statementData.dataOrNull() - ?: throw IllegalStateException("onClickSave: statement data cannot be null after validation") + val assignment = uiState.value.statementData.dataOrNull() ?: return launchWithLoadingIndicator { schoolDataSource.xapiResource.statements.post( @@ -355,5 +352,7 @@ class AssignmentEditViewModel( const val ACTIVITY_ID_PATH = "xapi/activities/assignment" + const val STUDENTS = "students" + } } From 750923e49ce4e92bcc90adbfc56925e10779fc57 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 12:57:09 +0530 Subject: [PATCH 128/130] Simplify string template for `XapiGroup` name in `studentsXapiGroup`. --- .../src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt index e8291a020..27d180228 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt @@ -13,7 +13,7 @@ import world.respect.lib.xapi.model.XapiObjectType fun XapiActivity.studentsXapiGroup(): XapiGroup { val className = definition?.name?.values?.firstOrNull() return XapiGroup( - name = "${className ?: ""} students", + name = "$className students", account = XapiAccount( homePage = id, name = "students" From a74852bd839275592b3c198d07977b9d1fa4cba9 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 15:35:24 +0530 Subject: [PATCH 129/130] Simplify string template for `XapiGroup` name in `studentsXapiGroup`. --- .../commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt index 27d180228..08efd039c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt @@ -4,6 +4,7 @@ import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.model.XapiObjectType +import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel.Companion.STUDENTS /** @@ -13,12 +14,13 @@ import world.respect.lib.xapi.model.XapiObjectType fun XapiActivity.studentsXapiGroup(): XapiGroup { val className = definition?.name?.values?.firstOrNull() return XapiGroup( - name = "$className students", + name = "$className $STUDENTS", account = XapiAccount( homePage = id, - name = "students" + name = STUDENTS ), objectType = XapiObjectType.Group, ) } + From dbffdc8cd116eb9198f72d0be112132fa39cc9a3 Mon Sep 17 00:00:00 2001 From: mandviverma Date: Fri, 19 Jun 2026 15:37:19 +0530 Subject: [PATCH 130/130] Move `STUDENTS` constant from `AssignmentEditViewModel` to `UpdateClazzStudentXapiGroupUseCase`. --- .../domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt | 4 ++++ .../commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt | 2 +- .../viewmodel/assignment/edit/AssignmentEditViewModel.kt | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt index 3ebe561e5..2e2622b2f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/domain/enrollments/UpdateClazzStudentXapiGroupUseCase.kt @@ -69,4 +69,8 @@ class UpdateClazzStudentXapiGroupUseCase( ) ) } + + companion object{ + const val STUDENTS = "students" + } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt index 08efd039c..b8a347308 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/ext/ClazzExt.kt @@ -4,7 +4,7 @@ import world.respect.lib.xapi.model.XapiAccount import world.respect.lib.xapi.model.XapiActivity import world.respect.lib.xapi.model.XapiGroup import world.respect.lib.xapi.model.XapiObjectType -import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel.Companion.STUDENTS +import world.respect.shared.domain.enrollments.UpdateClazzStudentXapiGroupUseCase.Companion.STUDENTS /** diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index e75ab3f7e..ceb953d18 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -40,6 +40,7 @@ import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementParam import world.respect.libutil.ext.appendEndpointSegments import world.respect.libutil.ext.isNullOrAllBlank import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.domain.enrollments.UpdateClazzStudentXapiGroupUseCase.Companion.STUDENTS import world.respect.shared.domain.opds.getxapiactivityid.GetXapiActivityForPublicationUseCase import world.respect.shared.domain.xapi.createBlankAssignmentStatement import world.respect.shared.generated.resources.Res @@ -352,7 +353,5 @@ class AssignmentEditViewModel( const val ACTIVITY_ID_PATH = "xapi/activities/assignment" - const val STUDENTS = "students" - } }