Refactor student selection screen (#2087)

This commit is contained in:
Mikołaj Pich 2023-01-01 20:26:32 +01:00 committed by GitHub
parent 83974b6550
commit 897eac050a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1052 additions and 349 deletions

View File

@ -186,7 +186,7 @@ ext {
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.8.3" implementation "io.github.wulkanowy:sdk:a3b97edd48"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'

View File

@ -0,0 +1,87 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.*
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mapper.mapSemesters
import java.time.Instant
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser
fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser(
email = email,
login = login,
password = password,
baseUrl = baseUrl,
loginType = loginType,
symbols = symbols.map { registerSymbol ->
RegisterSymbol(
symbol = registerSymbol.symbol,
error = registerSymbol.error,
userName = registerSymbol.userName,
schools = registerSymbol.schools.map {
RegisterUnit(
userLoginId = it.userLoginId,
schoolId = it.schoolId,
schoolName = it.schoolName,
schoolShortName = it.schoolShortName,
parentIds = it.parentIds,
studentIds = it.studentIds,
employeeIds = it.employeeIds,
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapSemesters()
.mapToEntities(registerSubject.studentId),
)
},
)
}
)
}
)
fun RegisterStudent.mapToStudentWithSemesters(
user: RegisterUser,
symbol: RegisterSymbol,
unit: RegisterUnit,
colors: List<Long>,
): StudentWithSemesters = StudentWithSemesters(
semesters = semesters,
student = Student(
email = user.login, // for compatibility
userName = symbol.userName,
userLoginId = unit.userLoginId,
isParent = isParent,
className = className,
classId = classId,
studentId = studentId,
symbol = symbol.symbol,
loginType = user.loginType.name,
schoolName = unit.schoolName,
schoolShortName = unit.schoolShortName,
schoolSymbol = unit.schoolId,
studentName = "$studentName $studentSurname",
loginMode = Sdk.Mode.SCRAPPER.name,
scrapperBaseUrl = user.baseUrl,
mobileBaseUrl = "",
certificateKey = "",
privateKey = "",
password = user.password,
isCurrent = false,
registrationDate = Instant.now(),
).apply {
avatarColor = colors.random()
},
)

View File

@ -0,0 +1,43 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.scrapper.Scrapper
data class RegisterUser(
val email: String,
val password: String,
val login: String, // may be the same as email
val baseUrl: String,
val loginType: Scrapper.LoginType,
val symbols: List<RegisterSymbol>,
) : java.io.Serializable
data class RegisterSymbol(
val symbol: String,
val error: Throwable?,
val userName: String,
val schools: List<RegisterUnit>,
) : java.io.Serializable
data class RegisterUnit(
val userLoginId: Int,
val schoolId: String,
val schoolName: String,
val schoolShortName: String,
val parentIds: List<Int>,
val studentIds: List<Int>,
val employeeIds: List<Int>,
val error: Throwable?,
val students: List<RegisterStudent>,
) : java.io.Serializable
data class RegisterStudent(
val studentId: Int,
val studentName: String,
val studentSecondName: String,
val studentSurname: String,
val className: String,
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
) : java.io.Serializable

View File

@ -11,6 +11,8 @@ import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
@ -52,6 +54,14 @@ class StudentRepository @Inject constructor(
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar) .mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getUserSubjectsFromScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(
email: String, email: String,
password: String, password: String,

View File

@ -8,10 +8,11 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
import androidx.fragment.app.commit import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.ActivityLoginBinding import io.github.wulkanowy.databinding.ActivityLoginBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
@ -76,8 +77,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
openFragment(LoginSymbolFragment.newInstance(loginData)) openFragment(LoginSymbolFragment.newInstance(loginData))
} }
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
openFragment(LoginStudentSelectFragment.newInstance(studentsWithSemesters)) openFragment(LoginStudentSelectFragment.newInstance(loginData, registerUser))
} }
fun navigateToNotifications() { fun navigateToNotifications() {
@ -105,6 +106,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
} }
private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) { private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) {
supportFragmentManager.popBackStack(fragment::class.java.name, POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.commit { supportFragmentManager.commit {
replace(R.id.loginContainer, fragment) replace(R.id.loginContainer, fragment)
setReorderingAllowed(true) setReorderingAllowed(true)

View File

@ -6,4 +6,5 @@ data class LoginData(
val login: String, val login: String,
val password: String, val password: String,
val baseUrl: String, val baseUrl: String,
val symbol: String?,
) : Serializable ) : Serializable

View File

@ -8,7 +8,7 @@ import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
@ -327,8 +327,8 @@ class LoginAdvancedFragment :
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData) (activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
} }
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
} }
override fun onResume() { override fun onResume() {

View File

@ -4,9 +4,15 @@ import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -149,11 +155,15 @@ class LoginAdvancedPresenter @Inject constructor(
val loginData = LoginData( val loginData = LoginData(
login = view?.formUsernameValue.orEmpty().trim(), login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(), password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim() baseUrl = view?.formHostValue.orEmpty().trim(),
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
) )
when (it.data.size) { when (it.data.size) {
0 -> view?.navigateToSymbol(loginData) 0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(it.data) else -> view?.navigateToStudentSelect(
loginData = loginData,
registerUser = it.data.toRegisterUser(loginData),
)
} }
} }
is Resource.Error -> { is Resource.Error -> {
@ -173,6 +183,58 @@ class LoginAdvancedPresenter @Inject constructor(
}.launch("login") }.launch("login")
} }
private fun List<StudentWithSemesters>.toRegisterUser(loginData: LoginData) = RegisterUser(
email = loginData.login,
password = loginData.password,
login = loginData.login,
baseUrl = loginData.baseUrl,
loginType = firstOrNull()?.student?.loginType?.let(
Scrapper.LoginType::valueOf
) ?: Scrapper.LoginType.AUTO,
symbols = this
.groupBy { students -> students.student.symbol }
.map { (symbol, students) ->
RegisterSymbol(
symbol = symbol,
error = null,
userName = "",
schools = students
.groupBy { student ->
Triple(
first = student.student.schoolSymbol,
second = student.student.userLoginId,
third = student.student.schoolShortName
)
}
.map { (groupKey, students) ->
val (schoolId, loginId, schoolName) = groupKey
RegisterUnit(
students = students.map {
RegisterStudent(
studentId = it.student.studentId,
studentName = it.student.studentName,
studentSecondName = it.student.studentName,
studentSurname = it.student.studentName,
className = it.student.className,
classId = it.student.classId,
isParent = it.student.isParent,
semesters = it.semesters,
)
},
userLoginId = loginId,
schoolId = schoolId,
schoolName = schoolName,
schoolShortName = schoolName,
parentIds = listOf(),
studentIds = listOf(),
employeeIds = listOf(),
error = null
)
}
)
},
)
private suspend fun getStudentsAppropriatesToLoginType(): List<StudentWithSemesters> { private suspend fun getStudentsAppropriatesToLoginType(): List<StudentWithSemesters> {
val email = view?.formUsernameValue.orEmpty() val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty() val password = view?.formPassValue.orEmpty()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.login.advanced package io.github.wulkanowy.ui.modules.login.advanced
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
@ -72,7 +73,7 @@ interface LoginAdvancedView : BaseView {
fun navigateToSymbol(loginData: LoginData) fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun setErrorPinRequired() fun setErrorPinRequired()

View File

@ -9,7 +9,7 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
@ -226,8 +226,8 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData) (activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
} }
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
} }
override fun openAdvancedLogin() { override fun openAdvancedLogin() {

View File

@ -93,7 +93,7 @@ class LoginFormPresenter @Inject constructor(
if (!validateCredentials(email, password, host)) return if (!validateCredentials(email, password, host)) return
resourceFlow { resourceFlow {
studentRepository.getStudentsScrapper( studentRepository.getUserSubjectsFromScrapper(
email = email, email = email,
password = password, password = password,
scrapperBaseUrl = host, scrapperBaseUrl = host,
@ -109,14 +109,14 @@ class LoginFormPresenter @Inject constructor(
} }
} }
.onResourceSuccess { .onResourceSuccess {
when (it.size) { val loginData = LoginData(email, password, host, symbol)
0 -> view?.navigateToSymbol(LoginData(email, password, host)) when (it.symbols.size) {
else -> view?.navigateToStudentSelect(it) 0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(loginData, it)
} }
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to true, "success" to true,
"students" to it.size,
"scrapperBaseUrl" to host, "scrapperBaseUrl" to host,
"error" to "No error" "error" to "No error"
) )
@ -134,7 +134,6 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent( analytics.logEvent(
"registration_form", "registration_form",
"success" to false, "success" to false,
"students" to -1,
"scrapperBaseUrl" to host, "scrapperBaseUrl" to host,
"error" to it.message.ifNullOrBlank { "No message" } "error" to it.message.ifNullOrBlank { "No message" }
) )

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
@ -60,7 +60,7 @@ interface LoginFormView : BaseView {
fun navigateToSymbol(loginData: LoginData) fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun openPrivacyPolicyPage() fun openPrivacyPolicyPage()

View File

@ -2,65 +2,182 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.ItemLoginStudentSelectBinding import io.github.wulkanowy.databinding.*
import javax.inject.Inject import javax.inject.Inject
@SuppressLint("SetTextI18n")
class LoginStudentSelectAdapter @Inject constructor() : class LoginStudentSelectAdapter @Inject constructor() :
RecyclerView.Adapter<LoginStudentSelectAdapter.ItemViewHolder>() { ListAdapter<LoginStudentSelectItem, RecyclerView.ViewHolder>(Differ) {
private val checkedList = mutableMapOf<Int, Boolean>() override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
var items = emptyList<Pair<StudentWithSemesters, Boolean>>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
set(value) { val inflater = LayoutInflater.from(parent.context)
field = value return when (LoginStudentSelectItemType.values()[viewType]) {
checkedList.clear() LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
)
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.HELP -> HelpViewHolder(
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
)
}
} }
var onClickListener: (StudentWithSemesters, alreadySaved: Boolean) -> Unit = { _, _ -> } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EmptySymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.EmptySymbolsHeader)
is SymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SymbolHeader)
is SchoolHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SchoolHeader)
is StudentViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Student)
is HelpViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Help)
}
}
override fun getItemCount() = items.size private class EmptySymbolsHeaderViewHolder(
private val binding: ItemLoginStudentSelectEmptySymbolHeaderBinding,
) : RecyclerView.ViewHolder(binding.root) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( fun bind(item: LoginStudentSelectItem.EmptySymbolsHeader) {
ItemLoginStudentSelectBinding.inflate(LayoutInflater.from(parent.context), parent, false) with(binding) {
) loginStudentSelectEmptySymbolChevron.rotation = if (item.isExpanded) 270f else 90f
root.setOnClickListener { item.onClick() }
}
}
}
@SuppressLint("SetTextI18n") private class SymbolsHeaderViewHolder(
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { private val binding: ItemLoginStudentSelectHeaderSymbolBinding,
val (studentAndSemesters, alreadySaved) = items[position] ) : RecyclerView.ViewHolder(binding.root) {
val student = studentAndSemesters.student
val semesters = studentAndSemesters.semesters fun bind(item: LoginStudentSelectItem.SymbolHeader) {
with(binding) {
loginStudentSelectHeaderSymbolValue.text = buildString {
append(root.context.getString(R.string.mobile_device_symbol))
append(": ")
append(item.humanReadableName ?: item.symbol.symbol)
if (!item.humanReadableName.isNullOrBlank()) {
append(" (${item.symbol.symbol})")
}
}
loginStudentSelectHeaderSymbolUsername.text = item.symbol.userName
loginStudentSelectHeaderSymbolUsername.isVisible = item.symbol.userName.isNotBlank()
loginStudentSelectHeaderSymbolError.text = item.symbol.error?.message
loginStudentSelectHeaderSymbolError.isVisible = item.symbol.error != null
loginStudentSelectHeaderSymbolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.symbol.error != null) {
root.setOnClickListener { item.onClick(item.symbol) }
} else root.setOnClickListener(null)
}
}
}
private class SchoolHeaderViewHolder(
private val binding: ItemLoginStudentSelectHeaderSchoolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SchoolHeader) {
with(binding) {
loginStudentSelectHeaderSchoolName.text = buildString {
append(item.unit.schoolName.trim())
append(" (")
append(item.unit.schoolShortName)
append(")")
}
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
loginStudentSelectHeaderSchoolError.isVisible = item.unit.error != null
loginStudentSelectHeaderSchoolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.unit.error != null) {
root.setOnClickListener { item.onClick(item.unit) }
} else root.setOnClickListener(null)
}
}
}
private class StudentViewHolder(
private val binding: ItemLoginStudentSelectStudentBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Student) {
val student = item.student
val semesters = student.semesters
val diary = semesters.maxByOrNull { it.semesterId } val diary = semesters.maxByOrNull { it.semesterId }
with(holder.binding) { with(binding) {
loginItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}" loginItemName.text = "${student.studentName} ${student.studentSurname}"
loginItemSchool.text = student.schoolName loginItemName.isEnabled = item.isEnabled
loginItemName.isEnabled = !alreadySaved loginItemSignedIn.text = if (!item.isEnabled) {
loginItemSchool.isEnabled = !alreadySaved root.context.getString(R.string.login_signed_in)
loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE } else diary?.diaryName
with(loginItemCheck) { with(loginItemCheck) {
isEnabled = !alreadySaved
keyListener = null keyListener = null
isChecked = checkedList[position] ?: false isEnabled = item.isEnabled
isChecked = item.isSelected || !item.isEnabled
} }
root.isEnabled = item.isEnabled
root.setOnClickListener { root.setOnClickListener {
onClickListener(studentAndSemesters, alreadySaved) item.onClick(item)
with(loginItemCheck) {
if (isEnabled) {
isChecked = !isChecked
checkedList[position] = isChecked
}
} }
} }
} }
} }
class ItemViewHolder(val binding: ItemLoginStudentSelectBinding) : private class HelpViewHolder(
RecyclerView.ViewHolder(binding.root) private val binding: ItemLoginStudentSelectHelpBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Help) {
with(binding) {
loginStudentSelectHelpSymbol.isVisible = item.isSymbolButtonVisible
loginStudentSelectHelpSymbol.setOnClickListener { item.onEnterSymbolClick() }
loginStudentSelectHelpMail.setOnClickListener { item.onContactUsClick() }
loginStudentSelectHelpDiscord.setOnClickListener { item.onDiscordClick() }
}
}
}
private object Differ : ItemCallback<LoginStudentSelectItem>() {
override fun areItemsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = when {
oldItem is LoginStudentSelectItem.EmptySymbolsHeader && newItem is LoginStudentSelectItem.EmptySymbolsHeader -> true
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
oldItem.symbol == newItem.symbol
}
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
oldItem.student == newItem.student
}
else -> oldItem == newItem
}
override fun areContentsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = oldItem == newItem
}
} }

View File

@ -2,17 +2,16 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
@ -36,12 +35,23 @@ class LoginStudentSelectFragment :
@Inject @Inject
lateinit var preferencesRepository: PreferencesRepository lateinit var preferencesRepository: PreferencesRepository
companion object { private lateinit var symbolsNames: Array<String>
const val ARG_STUDENTS = "STUDENTS" private lateinit var symbolsValues: Array<String>
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) = override val symbols: Map<String, String> by lazy {
symbolsValues.zip(symbolsNames).toMap()
}
companion object {
private const val ARG_LOGIN = "LOGIN"
private const val ARG_STUDENTS = "STUDENTS"
fun newInstance(loginData: LoginData, registerUser: RegisterUser) =
LoginStudentSelectFragment().apply { LoginStudentSelectFragment().apply {
arguments = bundleOf(ARG_STUDENTS to studentsWithSemesters) arguments = bundleOf(
ARG_LOGIN to loginData,
ARG_STUDENTS to registerUser,
)
} }
} }
@ -49,34 +59,32 @@ class LoginStudentSelectFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentLoginStudentSelectBinding.bind(view) binding = FragmentLoginStudentSelectBinding.bind(view)
symbolsNames = resources.getStringArray(R.array.symbols)
symbolsValues = resources.getStringArray(R.array.symbols_values)
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
students = requireArguments().serializable(ARG_STUDENTS), loginData = requireArguments().serializable(ARG_LOGIN),
registerUser = requireArguments().serializable(ARG_STUDENTS),
) )
} }
override fun initView() { override fun initView() {
(requireActivity() as LoginActivity).showActionBar(true) (requireActivity() as LoginActivity).showActionBar(true)
loginAdapter.onClickListener = presenter::onItemSelected
with(binding) { with(binding) {
loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() } loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() }
loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() } loginStudentSelectRecycler.adapter = loginAdapter
loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() }
with(loginStudentSelectRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = loginAdapter
}
} }
} }
override fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>) { override fun updateData(data: List<LoginStudentSelectItem>) {
with(loginAdapter) { loginAdapter.submitList(data)
items = data
notifyDataSetChanged()
} }
override fun navigateToSymbol(loginData: LoginData) {
(requireActivity() as LoginActivity).navigateToSymbolFragment(loginData)
} }
override fun navigateToNext() { override fun navigateToNext() {
@ -84,26 +92,17 @@ class LoginStudentSelectFragment :
} }
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE binding.loginStudentSelectProgress.isVisible = show
} }
override fun showContent(show: Boolean) { override fun showContent(show: Boolean) {
binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE binding.loginStudentSelectContent.isVisible = show
} }
override fun enableSignIn(enable: Boolean) { override fun enableSignIn(enable: Boolean) {
binding.loginStudentSelectSignIn.isEnabled = enable binding.loginStudentSelectSignIn.isEnabled = enable
} }
override fun showContact(show: Boolean) {
binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
override fun openDiscordInvite() { override fun openDiscordInvite() {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
} }
@ -124,4 +123,9 @@ class LoginStudentSelectFragment :
) )
) )
} }
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
} }

View File

@ -0,0 +1,50 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
sealed class LoginStudentSelectItem(val type: LoginStudentSelectItemType) {
data class EmptySymbolsHeader(
val isExpanded: Boolean,
val onClick: () -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER)
data class SymbolHeader(
val symbol: RegisterSymbol,
val humanReadableName: String?,
val isErrorExpanded: Boolean,
val onClick: (RegisterSymbol) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SYMBOL_HEADER)
data class SchoolHeader(
val unit: RegisterUnit,
val isErrorExpanded: Boolean,
val onClick: (RegisterUnit) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SCHOOL_HEADER)
data class Student(
val symbol: RegisterSymbol,
val unit: RegisterUnit,
val student: RegisterStudent,
val isEnabled: Boolean,
val isSelected: Boolean,
val onClick: (Student) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.STUDENT)
data class Help(
val onEnterSymbolClick: () -> Unit,
val onContactUsClick: () -> Unit,
val onDiscordClick: () -> Unit,
val isSymbolButtonVisible: Boolean,
) : LoginStudentSelectItem(LoginStudentSelectItemType.HELP)
}
enum class LoginStudentSelectItemType {
EMPTY_SYMBOLS_HEADER,
SYMBOL_HEADER,
SCHOOL_HEADER,
STUDENT,
HELP,
}

View File

@ -1,15 +1,23 @@
package io.github.wulkanowy.ui.modules.login.studentselect package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
@ -19,18 +27,30 @@ class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
private val appInfo: AppInfo,
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null private var lastError: Throwable? = null
private val selectedStudents = mutableListOf<StudentWithSemesters>() private lateinit var registerUser: RegisterUser
private lateinit var loginData: LoginData
fun onAttachView(view: LoginStudentSelectView, students: List<StudentWithSemesters>) { private lateinit var students: List<StudentWithSemesters>
private var isEmptySymbolsExpanded = false
private var expandedSymbolError: RegisterSymbol? = null
private var expandedSchoolError: RegisterUnit? = null
private val selectedStudents = mutableListOf<LoginStudentSelectItem.Student>()
fun onAttachView(
view: LoginStudentSelectView,
loginData: LoginData,
registerUser: RegisterUser,
) {
super.onAttachView(view) super.onAttachView(view)
with(view) { with(view) {
initView() initView()
showContact(false)
enableSignIn(false) enableSignIn(false)
loginErrorHandler.onStudentDuplicate = { loginErrorHandler.onStudentDuplicate = {
showMessage(it) showMessage(it)
@ -38,50 +58,171 @@ class LoginStudentSelectPresenter @Inject constructor(
} }
} }
if (students.size == 1) registerStudents(students) this.loginData = loginData
loadData(students) this.registerUser = registerUser
loadData()
} }
private fun loadData() {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
students = it.dataOrNull.orEmpty()
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> refreshItems()
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
refreshItems()
}
}
}.launch()
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.symbol }) {
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.symbol }))
}
addAll(createNotEmptySymbolItems(notEmptySymbols, students))
addAll(createEmptySymbolItems(emptySymbols, notEmptySymbols.isNotEmpty()))
val helpItem = LoginStudentSelectItem.Help(
onEnterSymbolClick = ::onEnterSymbol,
onContactUsClick = ::onEmailClick,
onDiscordClick = ::onDiscordClick,
isSymbolButtonVisible = "login" !in loginData.baseUrl,
)
add(helpItem)
}
private fun createNotEmptySymbolItems(
notEmptySymbols: List<RegisterSymbol>,
students: List<StudentWithSemesters>,
) = buildList {
notEmptySymbols.forEach { registerSymbol ->
val symbolHeader = LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
add(symbolHeader)
registerSymbol.schools.forEach { registerUnit ->
val schoolHeader = LoginStudentSelectItem.SchoolHeader(
unit = registerUnit,
isErrorExpanded = expandedSchoolError == registerUnit,
onClick = ::onUnitItemClick,
)
add(schoolHeader)
registerUnit.students.forEach {
add(createStudentItem(it, registerSymbol, registerUnit, students))
}
}
}
}
private fun createStudentItem(
student: RegisterStudent,
symbol: RegisterSymbol,
school: RegisterUnit,
students: List<StudentWithSemesters>,
) = LoginStudentSelectItem.Student(
symbol = symbol,
unit = school,
student = student,
onClick = ::onItemSelected,
isEnabled = students.none {
it.student.email == registerUser.login
&& it.student.symbol == symbol.symbol
&& it.student.studentId == student.studentId
&& it.student.schoolSymbol == school.schoolId
&& it.student.classId == student.classId
},
isSelected = selectedStudents
.filter { it.symbol.symbol == symbol.symbol }
.filter { it.unit.schoolId == school.schoolId }
.filter { it.student.studentId == student.studentId }
.filter { it.student.classId == student.classId }
.size == 1,
)
private fun createEmptySymbolItems(
emptySymbols: List<RegisterSymbol>,
isNotEmptySymbolsExist: Boolean,
) = buildList {
val filteredEmptySymbols = emptySymbols.filter {
it.error !is AccountPermissionException
}.ifEmpty { emptySymbols.takeIf { !isNotEmptySymbolsExist }.orEmpty() }
if (filteredEmptySymbols.isNotEmpty() && isNotEmptySymbolsExist) {
val emptyHeader = LoginStudentSelectItem.EmptySymbolsHeader(
isExpanded = isEmptySymbolsExpanded,
onClick = ::onEmptySymbolsToggle,
)
add(emptyHeader)
if (isEmptySymbolsExpanded) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
if (filteredEmptySymbols.isNotEmpty() && !isNotEmptySymbolsExist) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
private fun createEmptySymbolItem(registerSymbol: RegisterSymbol) =
LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
fun onSignIn() { fun onSignIn() {
registerStudents(selectedStudents) registerStudents(selectedStudents)
} }
fun onItemSelected(studentWithSemester: StudentWithSemesters, alreadySaved: Boolean) { private fun onEmptySymbolsToggle() {
if (alreadySaved) return isEmptySymbolsExpanded = !isEmptySymbolsExpanded
refreshItems()
}
private fun onItemSelected(item: LoginStudentSelectItem.Student) {
if (!item.isEnabled) return
selectedStudents selectedStudents
.removeAll { it == studentWithSemester } .removeAll {
.let { if (!it) selectedStudents.add(studentWithSemester) } it.student.studentId == item.student.studentId &&
it.student.classId == item.student.classId &&
it.unit.schoolId == item.unit.schoolId &&
it.symbol.symbol == item.symbol.symbol
}
.let { if (!it) selectedStudents.add(item) }
view?.enableSignIn(selectedStudents.isNotEmpty()) view?.enableSignIn(selectedStudents.isNotEmpty())
refreshItems()
} }
private fun compareStudents(a: Student, b: Student): Boolean { private fun onSymbolItemClick(symbol: RegisterSymbol) {
return a.email == b.email expandedSymbolError = if (symbol != expandedSymbolError) symbol else null
&& a.symbol == b.symbol refreshItems()
&& a.studentId == b.studentId
&& a.schoolSymbol == b.schoolSymbol
&& a.classId == b.classId
} }
private fun loadData(studentsWithSemesters: List<StudentWithSemesters>) { private fun onUnitItemClick(unit: RegisterUnit) {
resetSelectedState() expandedSchoolError = if (unit != expandedSchoolError) unit else null
refreshItems()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> view?.updateData(studentsWithSemesters.map { studentWithSemesters ->
studentWithSemesters to it.data.any { item ->
compareStudents(studentWithSemesters.student, item.student)
}
})
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
view?.updateData(studentsWithSemesters.map { student -> student to false })
}
}
}.launch()
} }
private fun resetSelectedState() { private fun resetSelectedState() {
@ -89,7 +230,20 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.enableSignIn(false) view?.enableSignIn(false)
} }
private fun registerStudents(studentsWithSemesters: List<StudentWithSemesters>) { private fun refreshItems() {
view?.updateData(createItems())
}
private fun registerStudents(students: List<LoginStudentSelectItem>) {
val studentsWithSemesters = students
.filterIsInstance<LoginStudentSelectItem.Student>().map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow { studentRepository.saveStudents(studentsWithSemesters) } resourceFlow { studentRepository.saveStudents(studentsWithSemesters) }
.logResourceStatus("registration") .logResourceStatus("registration")
.onEach { .onEach {
@ -107,7 +261,6 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.apply { view?.apply {
showProgress(false) showProgress(false)
showContent(true) showContent(true)
showContact(true)
} }
lastError = it.error lastError = it.error
loginErrorHandler.dispatch(it.error) loginErrorHandler.dispatch(it.error)
@ -117,12 +270,22 @@ class LoginStudentSelectPresenter @Inject constructor(
}.launch("register") }.launch("register")
} }
fun onDiscordClick() { private fun onEnterSymbol() {
view?.navigateToSymbol(loginData)
}
private fun onDiscordClick() {
view?.openDiscordInvite() view?.openDiscordInvite()
} }
fun onEmailClick() { private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" }) view?.openEmail(lastError?.message.ifNullOrBlank {
registerUser.symbols.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}.filterNotNull().distinct().joinToString("; ") {
it.take(46) + "..."
}.ifEmpty { "blank" }
})
} }
private fun logRegisterEvent( private fun logRegisterEvent(

View File

@ -1,13 +1,17 @@
package io.github.wulkanowy.ui.modules.login.studentselect package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginStudentSelectView : BaseView { interface LoginStudentSelectView : BaseView {
val symbols: Map<String, String>
fun initView() fun initView()
fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>) fun updateData(data: List<LoginStudentSelectItem>)
fun navigateToSymbol(loginData: LoginData)
fun navigateToNext() fun navigateToNext()
@ -17,8 +21,6 @@ interface LoginStudentSelectView : BaseView {
fun enableSignIn(enable: Boolean) fun enableSignIn(enable: Boolean)
fun showContact(show: Boolean)
fun openDiscordInvite() fun openDiscordInvite()
fun openEmail(lastError: String) fun openEmail(lastError: String)

View File

@ -12,7 +12,7 @@ import androidx.core.text.parseAsHtml
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
@ -42,6 +42,8 @@ class LoginSymbolFragment :
} }
} }
override val symbolValue: String? get() = binding.loginSymbolName.text?.toString()
override val symbolNameError: CharSequence? override val symbolNameError: CharSequence?
get() = binding.loginSymbolNameLayout.error get() = binding.loginSymbolNameLayout.error
@ -58,7 +60,7 @@ class LoginSymbolFragment :
(requireActivity() as LoginActivity).showActionBar(true) (requireActivity() as LoginActivity).showActionBar(true)
with(binding) { with(binding) {
loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) } loginSymbolSignIn.setOnClickListener { presenter.attemptLogin() }
loginSymbolFaq.setOnClickListener { presenter.onFaqClick() } loginSymbolFaq.setOnClickListener { presenter.onFaqClick() }
loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() } loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() }
@ -92,9 +94,13 @@ class LoginSymbolFragment :
} }
override fun setErrorSymbolRequire() { override fun setErrorSymbolRequire() {
binding.loginSymbolNameLayout.apply { setErrorSymbol(getString(R.string.error_field_required))
}
override fun setErrorSymbol(message: String) {
with(binding.loginSymbolNameLayout) {
requestFocus() requestFocus()
error = getString(R.string.error_field_required) error = message
} }
} }
@ -125,8 +131,8 @@ class LoginSymbolFragment :
binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE
} }
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) { override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters) (activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {

View File

@ -1,9 +1,12 @@
package io.github.wulkanowy.ui.modules.login.symbol package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -23,9 +26,14 @@ class LoginSymbolPresenter @Inject constructor(
lateinit var loginData: LoginData lateinit var loginData: LoginData
private var registerUser: RegisterUser? = null
fun onAttachView(view: LoginSymbolView, loginData: LoginData) { fun onAttachView(view: LoginSymbolView, loginData: LoginData) {
super.onAttachView(view) super.onAttachView(view)
this.loginData = loginData this.loginData = loginData
loginErrorHandler.onBadCredentials = {
view.setErrorSymbol(it.orEmpty())
}
with(view) { with(view) {
initView() initView()
showContact(false) showContact(false)
@ -39,20 +47,24 @@ class LoginSymbolPresenter @Inject constructor(
view?.apply { if (symbolNameError != null) clearSymbolError() } view?.apply { if (symbolNameError != null) clearSymbolError() }
} }
fun attemptLogin(symbol: String) { fun attemptLogin() {
if (symbol.isBlank()) { if (view?.symbolValue.isNullOrBlank()) {
view?.setErrorSymbolRequire() view?.setErrorSymbolRequire()
return return
} }
loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(),
)
resourceFlow { resourceFlow {
studentRepository.getStudentsScrapper( studentRepository.getUserSubjectsFromScrapper(
email = loginData.login, email = loginData.login,
password = loginData.password, password = loginData.password,
scrapperBaseUrl = loginData.baseUrl, scrapperBaseUrl = loginData.baseUrl,
symbol = symbol, symbol = view?.symbolValue.orEmpty(),
) )
}.onEach { }.onEach {
registerUser = it.dataOrNull
when (it) { when (it) {
is Resource.Loading -> view?.run { is Resource.Loading -> view?.run {
Timber.i("Login with symbol started") Timber.i("Login with symbol started")
@ -61,7 +73,7 @@ class LoginSymbolPresenter @Inject constructor(
showContent(false) showContent(false)
} }
is Resource.Success -> { is Resource.Success -> {
when (it.data.size) { when (it.data.symbols.size) {
0 -> { 0 -> {
Timber.i("Login with symbol result: Empty student list") Timber.i("Login with symbol result: Empty student list")
view?.run { view?.run {
@ -71,15 +83,14 @@ class LoginSymbolPresenter @Inject constructor(
} }
else -> { else -> {
Timber.i("Login with symbol result: Success") Timber.i("Login with symbol result: Success")
view?.navigateToStudentSelect(requireNotNull(it.data)) view?.navigateToStudentSelect(loginData, requireNotNull(it.data))
} }
} }
analytics.logEvent( analytics.logEvent(
"registration_symbol", "registration_symbol",
"success" to true, "success" to true,
"students" to it.data.size,
"scrapperBaseUrl" to loginData.baseUrl, "scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol, "symbol" to view?.symbolValue,
"error" to "No error" "error" to "No error"
) )
} }
@ -90,7 +101,7 @@ class LoginSymbolPresenter @Inject constructor(
"success" to false, "success" to false,
"students" to -1, "students" to -1,
"scrapperBaseUrl" to loginData.baseUrl, "scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol, "symbol" to view?.symbolValue,
"error" to it.error.message.ifNullOrBlank { "No message" } "error" to it.error.message.ifNullOrBlank { "No message" }
) )
loginErrorHandler.dispatch(it.error) loginErrorHandler.dispatch(it.error)
@ -111,6 +122,12 @@ class LoginSymbolPresenter @Inject constructor(
} }
fun onEmailClick() { fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { "empty" }) view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank {
registerUser?.symbols?.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}?.filterNotNull()?.distinct()?.joinToString(";") {
it.take(46) + "..."
} ?: "blank"
})
} }
} }

View File

@ -1,10 +1,13 @@
package io.github.wulkanowy.ui.modules.login.symbol package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginSymbolView : BaseView { interface LoginSymbolView : BaseView {
val symbolValue: String?
val symbolNameError: CharSequence? val symbolNameError: CharSequence?
fun initView() fun initView()
@ -15,6 +18,8 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolRequire() fun setErrorSymbolRequire()
fun setErrorSymbol(message: String)
fun clearSymbolError() fun clearSymbolError()
fun clearAndFocusSymbol() fun clearAndFocusSymbol()
@ -27,7 +32,7 @@ interface LoginSymbolView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun showContact(show: Boolean) fun showContact(show: Boolean)

View File

@ -3,92 +3,14 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical"
tools:context=".ui.modules.login.studentselect.LoginStudentSelectFragment">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginStudentSelectProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginStudentSelectContent" android:id="@+id/loginStudentSelectContent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout
android:id="@+id/loginStudentSelectContact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<View
android:id="@+id/loginStudentSelectContactTopDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<TextView
android:id="@+id/loginStudentSelectContactHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="16dp"
android:gravity="center_horizontal"
android:text="@string/login_contact_header"
android:textSize="14sp"
app:fontFamily="sans-serif-medium" />
<LinearLayout
android:id="@+id/loginStudentSelectContactButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectContactEmail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:text="@string/login_contact_email"
app:icon="@drawable/ic_more_messages" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectContactDiscord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord" />
</LinearLayout>
<View
android:id="@+id/loginStudentSelectContactBottomDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<TextView <TextView
android:id="@+id/loginStudentSelectHeader" android:id="@+id/loginStudentSelectHeader"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -106,22 +28,24 @@
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler" app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectContact" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" /> app:layout_constraintVertical_chainStyle="packed" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginStudentSelectRecycler" android:id="@+id/loginStudentSelectRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="144dp" android:fadingEdge="vertical"
android:fadingEdgeLength="100dp"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constrainedHeight="true" app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectSignIn" app:layout_constraintBottom_toTopOf="@id/loginStudentSelectSignIn"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="432dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectHeader" app:layout_constraintTop_toBottomOf="@id/loginStudentSelectHeader"
tools:itemCount="6" tools:itemCount="33"
tools:listitem="@layout/item_login_student_select" /> tools:listitem="@layout/item_login_student_select_student" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectSignIn" android:id="@+id/loginStudentSelectSignIn"
@ -136,4 +60,12 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" /> app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginStudentSelectProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="?selectableItemBackground"
android:paddingHorizontal="16dp">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/login_student_select_empty_symbol_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp"
android:text="@string/login_other_search_locations"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/login_student_select_empty_symbol_chevron"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="16dp"
android:rotation="90"
android:src="@drawable/ic_chevron_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:textColorPrimary"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/login_student_select_header_school_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent"
tools:text="Publiczna szkoła Wulkanowego" />
<TextView
android:id="@+id/login_student_select_header_school_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/login_no_active_student"
android:textColor="?colorTimetableChange"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_school_name" />
<TextView
android:id="@+id/login_student_select_header_school_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?colorError"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_school_details"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/login_student_select_header_symbol_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent"
tools:text="powiatjaroslawski" />
<TextView
android:id="@+id/login_student_select_header_symbol_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?android:textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_symbol_value"
tools:text="Jan Kowalski" />
<TextView
android:id="@+id/login_student_select_header_symbol_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?colorError"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_symbol_username"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_symbol"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_symbol_enter"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/login_student_select_help_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_contact_header"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/login_student_select_help_mail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_student_select_help_symbol" />
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_mail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:contentDescription="@string/login_contact_email"
app:icon="@drawable/ic_more_messages"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintEnd_toEndOf="@id/login_student_select_help_title"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/login_student_select_help_title"
app:layout_constraintTop_toBottomOf="@id/login_student_select_help_symbol" />
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_discord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:contentDescription="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/login_student_select_help_mail"
app:layout_constraintTop_toTopOf="@id/login_student_select_help_mail" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:minHeight="72dp" android:minHeight="56dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter"> tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter">
@ -14,9 +14,10 @@
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="12dp" android:layout_marginStart="32dp"
android:layout_marginEnd="28dp" android:layout_marginEnd="28dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:clickable="false"
tools:text=" " /> tools:text=" " />
<TextView <TextView
@ -32,34 +33,18 @@
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
<TextView <TextView
android:id="@+id/loginItemSchool" android:id="@+id/loginItemSignedIn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/loginItemName" android:layout_below="@id/loginItemName"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/loginItemCheck" android:layout_toEndOf="@id/loginItemCheck"
android:ellipsize="end" android:ellipsize="end"
android:gravity="bottom"
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/loginItemSignedIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="20dp"
android:layout_below="@id/loginItemSchool"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/loginItemCheck"
android:ellipsize="end"
android:enabled="false" android:enabled="false"
android:gravity="bottom" android:gravity="bottom"
android:maxLines="1" android:maxLines="1"
android:minHeight="20dp"
android:text="@string/login_signed_in" android:text="@string/login_signed_in"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="14sp" android:textSize="14sp" />
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>

View File

@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Invalid symbol</string> <string name="login_invalid_symbol">Invalid symbol</string>
<string name="login_incorrect_symbol">Student not found. Validate the symbol and the chosen variation of the UONET+ register</string> <string name="login_incorrect_symbol">Student not found. Validate the symbol and the chosen variation of the UONET+ register</string>
<string name="login_duplicate_student">Selected student is already logged in</string> <string name="login_duplicate_student">Selected student is already logged in</string>
<string name="login_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the previous screen</string> <string name="login_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string>
<string name="login_select_student">Select students to log in to the application</string> <string name="login_select_student">Select students to log in to the application</string>
<string name="login_advanced">Other options</string> <string name="login_advanced">Other options</string>
<string name="login_advanced_warning_mobile_api">In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices</string> <string name="login_advanced_warning_mobile_api">In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices</string>
@ -74,6 +74,9 @@
<string name="login_recover">Recover</string> <string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string> <string name="login_signed_in">Student is already signed in</string>
<string name="login_host_standard">Standard</string> <string name="login_host_standard">Standard</string>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications--> <!--Notifications-->
<string name="notifications_header_title">Enable notifications</string> <string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string> <string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>

View File

@ -1,9 +1,9 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.mockk.* import io.mockk.*
@ -12,7 +12,6 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.io.IOException import java.io.IOException
import java.time.Instant
class LoginFormPresenterTest { class LoginFormPresenterTest {
@ -33,6 +32,15 @@ class LoginFormPresenterTest {
private lateinit var presenter: LoginFormPresenter private lateinit var presenter: LoginFormPresenter
private val registerUser = RegisterUser(
email = "",
password = "",
login = "",
baseUrl = "",
loginType = Scrapper.LoginType.AUTO,
symbols = listOf(),
)
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
@ -104,32 +112,9 @@ class LoginFormPresenterTest {
@Test @Test
fun loginTest() { fun loginTest() {
val studentTest = Student( coEvery {
email = "test@", repository.getUserSubjectsFromScrapper(any(), any(), any(), any())
password = "123", } returns registerUser
scrapperBaseUrl = "https://fakelog.cf/?email",
loginType = "AUTO",
studentName = "",
schoolSymbol = "",
schoolName = "",
studentId = 0,
classId = 1,
isCurrent = false,
symbol = "",
registrationDate = Instant.now(),
className = "",
mobileBaseUrl = "",
privateKey = "",
certificateKey = "",
loginMode = "",
userLoginId = 0,
schoolShortName = "",
isParent = false,
userName = ""
)
coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf(
StudentWithSemesters(studentTest, emptyList())
)
every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formUsernameValue } returns "@"
every { loginFormView.formPassValue } returns "123456" every { loginFormView.formPassValue } returns "123456"
@ -146,7 +131,9 @@ class LoginFormPresenterTest {
@Test @Test
fun loginEmptyTest() { fun loginEmptyTest() {
coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf() coEvery {
repository.getUserSubjectsFromScrapper(any(), any(), any(), any())
} returns registerUser
every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formUsernameValue } returns "@"
every { loginFormView.formPassValue } returns "123456" every { loginFormView.formPassValue } returns "123456"
every { loginFormView.formHostValue } returns "https://fakelog.cf/?email" every { loginFormView.formHostValue } returns "https://fakelog.cf/?email"
@ -162,7 +149,9 @@ class LoginFormPresenterTest {
@Test @Test
fun loginEmptyTwiceTest() { fun loginEmptyTwiceTest() {
coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf() coEvery {
repository.getUserSubjectsFromScrapper(any(), any(), any(), any())
} returns registerUser
every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formUsernameValue } returns "@"
every { loginFormView.formPassValue } returns "123456" every { loginFormView.formPassValue } returns "123456"
every { loginFormView.formHostValue } returns "https://fakelog.cf/?email" every { loginFormView.formHostValue } returns "https://fakelog.cf/?email"
@ -180,7 +169,14 @@ class LoginFormPresenterTest {
@Test @Test
fun loginErrorTest() { fun loginErrorTest() {
val testException = IOException("test") val testException = IOException("test")
coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } throws testException coEvery {
repository.getUserSubjectsFromScrapper(
any(),
any(),
any(),
any()
)
} throws testException
every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formUsernameValue } returns "@"
every { loginFormView.formPassValue } returns "123456" every { loginFormView.formPassValue } returns "123456"
every { loginFormView.formHostValue } returns "https://fakelog.cf/?email" every { loginFormView.formHostValue } returns "https://fakelog.cf/?email"

View File

@ -1,18 +1,22 @@
package io.github.wulkanowy.ui.modules.login.studentselect package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.mockk.* import io.mockk.*
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.time.Instant
class LoginStudentSelectPresenterTest { class LoginStudentSelectPresenterTest {
@ -22,7 +26,7 @@ class LoginStudentSelectPresenterTest {
@MockK(relaxed = true) @MockK(relaxed = true)
lateinit var errorHandler: LoginErrorHandler lateinit var errorHandler: LoginErrorHandler
@MockK(relaxed = true) @MockK
lateinit var loginStudentSelectView: LoginStudentSelectView lateinit var loginStudentSelectView: LoginStudentSelectView
@MockK @MockK
@ -34,33 +38,55 @@ class LoginStudentSelectPresenterTest {
@MockK(relaxed = true) @MockK(relaxed = true)
lateinit var syncManager: SyncManager lateinit var syncManager: SyncManager
private val appInfo = AppInfo()
private lateinit var presenter: LoginStudentSelectPresenter private lateinit var presenter: LoginStudentSelectPresenter
private val testStudent by lazy { private val loginData = LoginData(
Student( login = "",
email = "test", password = "",
password = "test123", baseUrl = "",
scrapperBaseUrl = "https://fakelog.cf", symbol = null,
loginType = "AUTO", )
symbol = "",
isCurrent = false, private val subject = RegisterStudent(
studentId = 0, studentId = 0,
schoolName = "", studentName = "",
schoolSymbol = "", studentSecondName = "",
classId = 1, studentSurname = "",
studentName = "", className = "",
registrationDate = Instant.now(), classId = 0,
className = "", isParent = false,
loginMode = "", semesters = listOf(),
certificateKey = "", )
privateKey = "",
mobileBaseUrl = "", private val school = RegisterUnit(
schoolShortName = "", userLoginId = 0,
userLoginId = 1, schoolId = "",
isParent = false, schoolName = "",
userName = "" schoolShortName = "",
parentIds = listOf(),
studentIds = listOf(),
employeeIds = listOf(),
error = null,
students = listOf(subject)
)
private val symbol = RegisterSymbol(
symbol = "",
error = null,
userName = "",
schools = listOf(school),
)
private val registerUser = RegisterUser(
email = "",
password = "",
login = "",
baseUrl = "",
loginType = Scrapper.LoginType.AUTO,
symbols = listOf(symbol),
) )
}
private val testException by lazy { RuntimeException("Problem") } private val testException by lazy { RuntimeException("Problem") }
@ -69,30 +95,44 @@ class LoginStudentSelectPresenterTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
clearMocks(studentRepository, loginStudentSelectView) clearMocks(studentRepository, loginStudentSelectView)
coEvery { studentRepository.getSavedStudents(false) } returns emptyList()
every { loginStudentSelectView.initView() } just Runs every { loginStudentSelectView.initView() } just Runs
every { loginStudentSelectView.showContact(any()) } just Runs every { loginStudentSelectView.symbols } returns emptyMap()
every { loginStudentSelectView.enableSignIn(any()) } just Runs every { loginStudentSelectView.enableSignIn(any()) } just Runs
every { loginStudentSelectView.showProgress(any()) } just Runs every { loginStudentSelectView.showProgress(any()) } just Runs
every { loginStudentSelectView.showContent(any()) } just Runs every { loginStudentSelectView.showContent(any()) } just Runs
presenter = LoginStudentSelectPresenter(studentRepository, errorHandler, syncManager, analytics) presenter = LoginStudentSelectPresenter(
presenter.onAttachView(loginStudentSelectView, emptyList()) studentRepository = studentRepository,
loginErrorHandler = errorHandler,
syncManager = syncManager,
analytics = analytics,
appInfo = appInfo,
)
} }
@Test @Test
fun initViewTest() { fun initViewTest() {
presenter.onAttachView(loginStudentSelectView, loginData, registerUser)
verify { loginStudentSelectView.initView() } verify { loginStudentSelectView.initView() }
} }
@Test @Test
fun onSelectedStudentTest() { fun onSelectedStudentTest() {
coEvery { val itemsSlot = slot<List<LoginStudentSelectItem>>()
studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) every { loginStudentSelectView.updateData(capture(itemsSlot)) } just Runs
} just Runs presenter.onAttachView(loginStudentSelectView, loginData, registerUser)
coEvery { studentRepository.saveStudents(any()) } just Runs
every { loginStudentSelectView.navigateToNext() } just Runs every { loginStudentSelectView.navigateToNext() } just Runs
presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false) itemsSlot.captured.filterIsInstance<LoginStudentSelectItem.Student>().first().let {
it.onClick(it)
}
presenter.onSignIn() presenter.onSignIn()
verify { loginStudentSelectView.showContent(false) } verify { loginStudentSelectView.showContent(false) }
@ -102,13 +142,15 @@ class LoginStudentSelectPresenterTest {
@Test @Test
fun onSelectedStudentErrorTest() { fun onSelectedStudentErrorTest() {
coEvery { val itemsSlot = slot<List<LoginStudentSelectItem>>()
studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) every { loginStudentSelectView.updateData(capture(itemsSlot)) } just Runs
} throws testException presenter.onAttachView(loginStudentSelectView, loginData, registerUser)
coEvery { studentRepository.logoutStudent(testStudent) } just Runs coEvery { studentRepository.saveStudents(any()) } throws testException
presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false) itemsSlot.captured.filterIsInstance<LoginStudentSelectItem.Student>().first().let {
it.onClick(it)
}
presenter.onSignIn() presenter.onSignIn()
verify { loginStudentSelectView.showContent(false) } verify { loginStudentSelectView.showContent(false) }