Écran pour ajouter un étudiant à une liste.

Sauvegarde d’une fiche de présence dans une session.
This commit is contained in:
biloute02 2024-01-13 17:09:45 +01:00
parent 035030ca0f
commit acb73364a1
28 changed files with 562 additions and 208 deletions

View file

@ -0,0 +1,22 @@
package com.example.palto
import android.nfc.Tag
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.palto.domain.Card
import com.example.palto.domain.Event
import com.example.palto.domain.User
class NfcViewModel: ViewModel() {
private var _tagId = MutableLiveData<Event<String>>()
val tagId: LiveData<Event<String>> = _tagId
@OptIn(ExperimentalStdlibApi::class)
fun setTag(tag: Tag) {
Log.d("Nfc", "A new tag has been set.")
_tagId.postValue(Event(tag.id.toHexString()))
}
}

View file

@ -11,8 +11,7 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.example.palto.databinding.ActivityPaltoBinding import com.example.palto.databinding.ActivityPaltoBinding
import com.example.palto.ui.NfcViewModel import com.example.palto.ui.CardViewModel
import com.example.palto.ui.UserViewModel
class PaltoActivity : AppCompatActivity() { class PaltoActivity : AppCompatActivity() {
@ -21,8 +20,6 @@ class PaltoActivity : AppCompatActivity() {
private val nfcViewModel: NfcViewModel by viewModels() private val nfcViewModel: NfcViewModel by viewModels()
private val userViewModel: UserViewModel by viewModels() { UserViewModel.Factory }
private lateinit var binding: ActivityPaltoBinding private lateinit var binding: ActivityPaltoBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -81,7 +78,7 @@ class PaltoActivity : AppCompatActivity() {
// Begin to read NFC Cards. // Begin to read NFC Cards.
nfcAdapter?.enableReaderMode( nfcAdapter?.enableReaderMode(
this, this,
nfcViewModel.tag::postValue, nfcViewModel::setTag,
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
null null
) )

View file

@ -1,10 +1,12 @@
package com.example.palto.domain package com.example.palto.domain
import java.io.Serializable import java.io.Serializable
import java.time.LocalTime
/** /**
* Data class that captures tokens for logged in users retrieved from LoginRepository * Data class that captures tokens for logged in users retrieved from LoginRepository
*/ */
data class Attendance( data class Attendance(
val date: String, val id: Int,
val access: String val student: User,
val date: LocalTime
) : Serializable ) : Serializable

View file

@ -1,22 +1,7 @@
package com.example.palto.domain package com.example.palto.domain
data class Card( data class Card(
val id: String, val id: Int,
val uid: ByteArray, val tagId: String,
val department: String, val user: User,
val owner: String )
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Card
return uid.contentEquals(other.uid)
}
override fun hashCode(): Int {
return uid.contentHashCode()
}
}

View file

@ -0,0 +1,18 @@
package com.example.palto.domain
class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): T = content
}

View file

@ -7,5 +7,6 @@ import java.io.Serializable
data class Session( data class Session(
val id: Int, val id: Int,
val name: String, val name: String,
var attendances: List<User> var attendances: List<Attendance>
// When the list is updated, it is replaced by a new one.
) )

View file

@ -4,10 +4,9 @@ package com.example.palto.domain
* Data class that captures user information for logged in users retrieved from LoginRepository * Data class that captures user information for logged in users retrieved from LoginRepository
*/ */
data class User( data class User(
//Not in the domain layer val id: Int,
//val id: String,
val username: String, val username: String,
val first_name: String, val firstName: String,
val last_name: String, val lastName: String,
val email: String val email: String
) )

View file

@ -0,0 +1,34 @@
package com.example.palto.ui
import android.nfc.Tag
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.palto.domain.Card
import com.example.palto.domain.User
/**
* CardViewModel maintain a list of cards application wide.
* May be converted in a repository.
*/
class CardViewModel: ViewModel() {
private var _cards = MutableLiveData<List<Card>>()
private val cards: LiveData<List<Card>> = _cards
fun createCard(user: User, tagId: String): Card {
val list = _cards.value ?: emptyList()
val card = Card(list.size, tagId, user)
_cards.value = list + card
Log.d("Palto", "CardViewModel: a card has been added into the list.")
return card
}
fun getCard(tagId: String): Card? {
val card = _cards.value?.find() {
it.tagId == tagId
}
return card
}
}

View file

@ -1,14 +0,0 @@
package com.example.palto.ui
import android.nfc.Tag
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.palto.domain.User
class NfcViewModel: ViewModel() {
val tag = MutableLiveData<Tag>()
private var _user : MutableLiveData<User> = MutableLiveData<User>()
val user : LiveData<User> = _user
}

View file

@ -4,75 +4,28 @@ import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import com.example.palto.domain.Card
import androidx.lifecycle.viewModelScope
import com.example.palto.R
import com.example.palto.data.repository.TokenRepository
import com.example.palto.data.repository.UserRepository
import com.example.palto.domain.User import com.example.palto.domain.User
import com.example.palto.ui.login.LoginResult
import kotlinx.coroutines.launch
class UserViewModel( /**
private val tokenRepository: TokenRepository, * UserViewModel maintain a list of users application wide.
private val userRepository: UserRepository * May be converted into a repository.
): ViewModel() { */
class UserViewModel: ViewModel() {
private var _result = MutableLiveData<LoginResult>() private var _users = MutableLiveData<List<User>>()
val result = _result as LiveData<LoginResult> val users : LiveData<List<User>> = _users
// User is initially set to null to be disconnected. fun createUser(username: String): User {
private var _user = MutableLiveData<User?>(null) val list = _users.value ?: emptyList()
val user = _user as LiveData<User?> val user = User(
id = list.size,
fun login( username = username,
hostname: String, firstName = "",
username: String, lastName = "",
password: String) { email = "")
_users.value = list + user
// Coroutine runs in background. Log.d("Palto", "UserViewModel: a user has been added into the list.")
viewModelScope.launch { return user
try {
tokenRepository.authenticate(hostname, username, password)
_user.value = User(
username,
"",
"",
""
)
_result.value = LoginResult(success = true)
} catch (e: Exception) {
Log.e("Palto", "Connection error: " + e.message)
_result.value = LoginResult(
success = false,
error = R.string.login_failed,
exception = e
)
}
}
}
fun loginAnonymous() {
_user.value = User(
"anonymous", "", "", ""
)
_result.value = LoginResult(success = true)
}
fun logout() {
_user.value = null
_result.value = LoginResult(success = false)
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return UserViewModel(
tokenRepository = TokenRepository(),
userRepository = UserRepository()
) as T
}
}
} }
} }

View file

@ -8,22 +8,16 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.example.palto.databinding.FragmentLoginBinding import com.example.palto.databinding.FragmentLoginBinding
import com.example.palto.ui.UserViewModel
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
// loginViewModel is used to update the login screen dynamically.
private val loginViewModel: LoginViewModel by viewModels()
// userViewModel is where the user is logged in, at the activity level. // userViewModel is where the user is logged in, at the activity level.
private val userViewModel: UserViewModel by activityViewModels() { UserViewModel.Factory } private val loginViewModel: LoginViewModel by activityViewModels() { LoginViewModel.Factory }
private lateinit var binding: FragmentLoginBinding private lateinit var binding: FragmentLoginBinding
@SuppressLint("SetTextI18n")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -36,7 +30,7 @@ class LoginFragment : Fragment() {
// Bind the login button. // Bind the login button.
binding.login.setOnClickListener { binding.login.setOnClickListener {
binding.loading.visibility = View.VISIBLE binding.loading.visibility = View.VISIBLE
userViewModel.login( loginViewModel.login(
binding.hostname.text.toString(), binding.hostname.text.toString(),
binding.username.text.toString(), binding.username.text.toString(),
binding.password.text.toString() binding.password.text.toString()
@ -45,16 +39,16 @@ class LoginFragment : Fragment() {
// Bind anonymous login clickable text. // Bind anonymous login clickable text.
binding.loginAnonymous.setOnClickListener { binding.loginAnonymous.setOnClickListener {
userViewModel.loginAnonymous() loginViewModel.loginAnonymous()
} }
// On result of logging. // On result of logging.
userViewModel.result.observe(viewLifecycleOwner) { loginViewModel.result.observe(viewLifecycleOwner) {
binding.loading.visibility = View.GONE binding.loading.visibility = View.GONE
if (it.success) { if (it.success) {
navController.popBackStack() navController.popBackStack()
} else if (it.error != null) { } else if (it.error != null) {
binding.loginError.text = "Exception : " + it.exception.toString() binding.loginError.text = "Exception : ${it.exception.toString()}"
Toast.makeText(activity, it.error, Toast.LENGTH_LONG).show() Toast.makeText(activity, it.error, Toast.LENGTH_LONG).show()
} }
} }

View file

@ -1,41 +1,106 @@
package com.example.palto.ui.login package com.example.palto.ui.login
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.palto.R
import com.example.palto.data.repository.TokenRepository
import com.example.palto.data.repository.UserRepository
import com.example.palto.domain.User
import kotlinx.coroutines.launch
/** /**
* View Model to check dynamically the values of the form. * LoginViewModel to access information about the logged in user and login form.
*/ */
class LoginViewModel() : ViewModel() { class LoginViewModel(
private val tokenRepository: TokenRepository,
private val userRepository: UserRepository
): ViewModel() {
private var _result = MutableLiveData<LoginResult>()
val result = _result as LiveData<LoginResult>
// User is initially set to null to be disconnected.
private var _user = MutableLiveData<User?>(null)
val user = _user as LiveData<User?>
/* /*
private val _loginFormState = MutableLiveData<LoginFormState>() private val _loginFormState = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginFormState val loginFormState: LiveData<LoginFormState> = _loginFormState
*/
fun loginDataChanged( fun login(
hostname: String, hostname: String,
username: String, username: String,
password: String) { password: String) {
if (!isHostNameValid(hostname)) {
_loginForm.value = LoginFormState(hostnameError = R.string.invalid_hostname) // Coroutine runs in background.
} else if (!isUserNameValid(username)) { viewModelScope.launch {
_loginForm.value = LoginFormState(usernameError = R.string.invalid_username) try {
} else if (!isPasswordValid(password)) { tokenRepository.authenticate(hostname, username, password)
_loginForm.value = LoginFormState(passwordError = R.string.invalid_password) _user.value = User(-1, username, "", "", "")
} else { _result.value = LoginResult(success = true)
_loginForm.value = LoginFormState(isDataValid = true) } catch (e: Exception) {
Log.e("Palto", "Connection error: " + e.message)
_result.value = LoginResult(
success = false,
error = R.string.login_failed,
exception = e
)
}
} }
} }
fun loginAnonymous() {
_user.value = User(-2, "anonymous", "", "", "")
_result.value = LoginResult(success = true)
}
fun logout() {
_user.value = null
_result.value = LoginResult(success = false)
}
/*
fun loginDataChanged(
hostname: String,
username: String,
password: String) {
if (!isHostNameValid(hostname)) {
_loginForm.value = LoginFormState(hostnameError = R.string.invalid_hostname)
} else if (!isUserNameValid(username)) {
_loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
} else if (!isPasswordValid(password)) {
_loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
} else {
_loginForm.value = LoginFormState(isDataValid = true)
}
}
private fun isHostNameValid(hostname: String): Boolean { private fun isHostNameValid(hostname: String): Boolean {
return hostname.isNotBlank() return hostname.isNotBlank()
} }
private fun isUserNameValid(username: String): Boolean { private fun isUserNameValid(username: String): Boolean {
return username.isNotBlank() return username.isNotBlank()
} }
private fun isPasswordValid(password: String): Boolean { private fun isPasswordValid(password: String): Boolean {
return password.length > 5 return password.length > 5
}
*/
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return LoginViewModel(
tokenRepository = TokenRepository(),
userRepository = UserRepository()
) as T
}
}
} }
*/
} }

View file

@ -12,7 +12,7 @@ import androidx.navigation.navGraphViewModels
import com.example.palto.R import com.example.palto.R
import com.example.palto.databinding.FragmentMenuListBinding import com.example.palto.databinding.FragmentMenuListBinding
import com.example.palto.domain.Session import com.example.palto.domain.Session
import com.example.palto.ui.UserViewModel import com.example.palto.ui.login.LoginViewModel
/** /**
* A fragment representing a list of Sessions. * A fragment representing a list of Sessions.
@ -22,8 +22,8 @@ class MenuFragment : Fragment() {
private val menuViewModel: MenuViewModel by private val menuViewModel: MenuViewModel by
navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory } navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory }
private val userViewModel: UserViewModel by private val loginViewModel: LoginViewModel by
activityViewModels() { UserViewModel.Factory } activityViewModels() { LoginViewModel.Factory }
// This property is only valid between onCreateView and onDestroyView // This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentMenuListBinding private lateinit var binding: FragmentMenuListBinding
@ -38,7 +38,7 @@ class MenuFragment : Fragment() {
val navController = findNavController() val navController = findNavController()
// Connect the user. // Connect the user.
userViewModel.user.observe(viewLifecycleOwner) { loginViewModel.user.observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
// Get sessions of the user from remote. // Get sessions of the user from remote.
} else { } else {

View file

@ -5,14 +5,16 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.example.palto.data.repository.TokenRepository import com.example.palto.domain.Attendance
import com.example.palto.data.repository.UserRepository
import com.example.palto.domain.Session import com.example.palto.domain.Session
import com.example.palto.ui.UserViewModel
/**
* ViewModel for accessing all the sessions created.
*/
class MenuViewModel() : ViewModel() { class MenuViewModel() : ViewModel() {
private var _sessions = MutableLiveData<List<Session>>(emptyList()) // A list of sessions.
private var _sessions = MutableLiveData<List<Session>>()
val sessions: LiveData<List<Session>> = _sessions val sessions: LiveData<List<Session>> = _sessions
fun createSession(name: String) { fun createSession(name: String) {
@ -26,6 +28,14 @@ class MenuViewModel() : ViewModel() {
Log.d("Palto", "MenuViewModel: A session has been added into the list.") Log.d("Palto", "MenuViewModel: A session has been added into the list.")
} }
fun getSession(id: Int): Session? {
return _sessions.value?.find { it.id == id }
}
fun setAttendanceListSession(id: Int, list: List<Attendance>) {
getSession(id)?.let { it.attendances = list }
}
companion object { companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {

View file

@ -0,0 +1,46 @@
package com.example.palto.ui.menu.new_session
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.menu.MenuView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.R
import com.example.palto.databinding.FragmentNewSessionBinding
import com.example.palto.ui.menu.MenuViewModel
/**
*
*/
class NewSessionFragment : Fragment() {
private val newSessionViewModel: NewSessionViewModel by viewModels()
private val menuViewModel: MenuViewModel by
navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory }
// This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentNewSessionBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNewSessionBinding.inflate(inflater, container, false)
// Bind the create button
binding.newSessionCreate.setOnClickListener {
menuViewModel.createSession(
name = binding.newSessionName.text.toString()
)
findNavController().popBackStack()
}
return binding.root
}
}

View file

@ -0,0 +1,8 @@
package com.example.palto.ui.menu.new_session
import androidx.lifecycle.ViewModel
/**
* ViewModel of the session creation form. Used for verification.
*/
class NewSessionViewModel() : ViewModel()

View file

@ -7,47 +7,46 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.example.palto.databinding.FragmentSessionItemBinding import com.example.palto.databinding.FragmentSessionItemBinding
import com.example.palto.domain.Attendance
import com.example.palto.domain.Card import com.example.palto.domain.Card
import java.time.format.DateTimeFormatter
/** /**
* A [ListAdapter] that can display [Card] items. * A [ListAdapter] that can display [Attendance] items.
*/ */
class SessionAdapter : class SessionAdapter : ListAdapter<Attendance, SessionAdapter.ViewHolder>(AttendanceDiffCallback) {
ListAdapter<Card, SessionAdapter.ViewHolder>(CardDiffCallback) {
inner class ViewHolder(binding: FragmentSessionItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val attendanceUsernameText: TextView = binding.attendanceUsername
private val attendanceDate: TextView = binding.attendanceDate
fun bind(attendance: Attendance) {
attendanceUsernameText.text = attendance.student.username
attendanceDate.text = attendance.date.format(DateTimeFormatter.ofPattern("HH:mm:ss"))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder( val binding = FragmentSessionItemBinding
FragmentSessionItemBinding.inflate( .inflate(LayoutInflater.from(parent.context), parent, false)
LayoutInflater.from(parent.context), return ViewHolder(binding)
parent,
false
)
)
} }
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
holder.cardId.text = item.uid.toHexString() holder.bind(item)
}
inner class ViewHolder(
binding: FragmentSessionItemBinding
) : RecyclerView.ViewHolder(binding.root) {
val cardId: TextView = binding.cardId
override fun toString(): String {
return super.toString() + " '" + cardId.text + "'"
}
} }
} }
object CardDiffCallback : DiffUtil.ItemCallback<Card>() { object AttendanceDiffCallback : DiffUtil.ItemCallback<Attendance>() {
override fun areItemsTheSame(oldItem: Card, newItem: Card): Boolean { override fun areItemsTheSame(oldItem: Attendance, newItem: Attendance): Boolean {
return oldItem == newItem return oldItem == newItem
} }
override fun areContentsTheSame(oldItem: Card, newItem: Card): Boolean { override fun areContentsTheSame(oldItem: Attendance, newItem: Attendance): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }
} }

View file

@ -5,12 +5,18 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import com.example.palto.ui.NfcViewModel import com.example.palto.NfcViewModel
import com.example.palto.R import com.example.palto.R
import com.example.palto.databinding.FragmentSessionListBinding import com.example.palto.databinding.FragmentSessionListBinding
import com.example.palto.ui.CardViewModel
import com.example.palto.ui.menu.MenuViewModel
/** /**
* A fragment representing a list of attendances. * A fragment representing a list of attendances.
@ -20,12 +26,16 @@ class SessionFragment : Fragment() {
private val sessionViewModel: SessionViewModel by private val sessionViewModel: SessionViewModel by
navGraphViewModels(R.id.nav_graph) { SessionViewModel.Factory } navGraphViewModels(R.id.nav_graph) { SessionViewModel.Factory }
private val nfcViewModel: NfcViewModel by private val menuViewModel: MenuViewModel by
activityViewModels() navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory }
private val cardViewModel: CardViewModel by
navGraphViewModels(R.id.nav_graph)
private val nfcViewModel: NfcViewModel by activityViewModels()
private var _binding: FragmentSessionListBinding? = null
// This property is only valid between onCreateView and onDestroyView // This property is only valid between onCreateView and onDestroyView
private val binding get() = _binding!! private lateinit var binding: FragmentSessionListBinding
/** /**
* Have the fragment instantiate the user interface. * Have the fragment instantiate the user interface.
@ -35,25 +45,62 @@ class SessionFragment : Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = FragmentSessionListBinding.inflate(inflater, container, false) binding = FragmentSessionListBinding.inflate(inflater, container, false)
// If the bundle value id of key "session" is not empty,
// set the attendance list to that of the selected session.
arguments?.getInt("session")?.let { id ->
val session = menuViewModel.getSession(id)
if (session != null) {
Log.d("Palto", "SessionFragment: Session id ${session.id} has been found.")
sessionViewModel.session = session
sessionViewModel.setAttendanceList(session.attendances)
}
}
// Set the adapter of the view for managing automatically the list of items on the screen. // Set the adapter of the view for managing automatically the list of items on the screen.
val adapter = SessionAdapter() val adapter = SessionAdapter()
binding.sessionList.adapter = adapter binding.sessionList.adapter = adapter
sessionViewModel.cards.observe(viewLifecycleOwner) { sessionViewModel.attendances.observe(viewLifecycleOwner) {
adapter.submitList(it) adapter.submitList(it)
} }
// Set the listener for a new NFC tag. // Set the listener for a new NFC tag.
nfcViewModel.tag.observe(viewLifecycleOwner) { nfcViewModel.tagId.observe(viewLifecycleOwner) {
Log.d("NFC", "The tag observers has been notified.")
sessionViewModel.insertCard(it) // If the NFC tag has not been handled.
it.getContentIfNotHandled()?.let { cardId ->
val card = cardViewModel.getCard(cardId)
// If a card with this tag exists, add this card.
if (card != null) {
sessionViewModel.addAttendance(card.user)
// Else go to the NewStudentFragment to create a new card and student.
} else {
val bundle = bundleOf("tagId" to cardId)
findNavController()
.navigate(R.id.action_sessionFragment_to_newStudentFragment, bundle)
}
}
} }
binding.createCard.setOnClickListener { // Manual add student button
//sessionViewModel. binding.addStudent.setOnClickListener {
findNavController().navigate(R.id.action_sessionFragment_to_newStudentFragment)
}
// Print the result of adding an attendance on the view.
sessionViewModel.result.observe(viewLifecycleOwner) {
// If the result has not been already shown
it.getContentIfNotHandled()?.let { result ->
Toast.makeText(
activity,
getString(result.message, result.username),
Toast.LENGTH_LONG).show()
}
} }
return binding.root return binding.root
} }
} }

View file

@ -0,0 +1,10 @@
package com.example.palto.ui.session
/**
* Authentication result : success is true if connected or error message with exception.
*/
data class SessionResult(
val success: Boolean,
val message: Int, // Id of the string resource to display to the user
val username: String
)

View file

@ -1,37 +1,74 @@
package com.example.palto.ui.session package com.example.palto.ui.session
import android.nfc.Tag
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.example.palto.data.repository.AttendanceRepository import com.example.palto.R
import com.example.palto.domain.Card import com.example.palto.domain.Attendance
import com.example.palto.domain.Event
import com.example.palto.domain.Session
import com.example.palto.domain.User
import java.time.LocalTime
/** /**
* ViewModel of a session which has a list of attendances. * ViewModel of a session which has a list of attendances.
*/ */
class SessionViewModel( class SessionViewModel() : ViewModel() {
private val attendanceRepository: AttendanceRepository
) : ViewModel() {
private val _cards = MutableLiveData<List<Card>>(emptyList()) private var _result = MutableLiveData<Event<SessionResult>>()
val cards = _cards as LiveData<List<Card>> val result: LiveData<Event<SessionResult>> = _result
fun insertCard(tag: Tag) { private var _attendances = MutableLiveData<List<Attendance>>()
val card = Card( val attendances: LiveData<List<Attendance>> = _attendances
"0",
tag.id, // The opened session which have been selected in the menu.
"tmp", var session: Session? = null
"tmp"
) /**
_cards.value = (_cards.value ?: emptyList()) + card * Add the [student] in the attendance list [attendances].
Log.d("PALTO", "SessionViewModel: a card has been added into the list.") * Return true if it has been added, else return false.
*/
fun addAttendance(student: User) {
val list = _attendances.value ?: emptyList()
// If the list already contains the user, return false
if (list.any { it.student == student }) {
Log.d("Palto", "SessionViewModel: User already in the list.")
_result.value = Event(SessionResult(
false,
R.string.session_user_already_added,
student.username
))
// Else create a new attendance and add it into the list.
} else {
val attendance = Attendance(
id = list.size,
student = student,
date = LocalTime.now()
)
// Add the attendance in the attendance list, and trigger the observers.
_attendances.value = list + attendance
// Saved the list in the session.
session?.attendances = list + attendance
_result.value = Event(SessionResult(
true,
R.string.session_user_added,
student.username
))
Log.d("Palto", "SessionViewModel: An attendance has been added into the list.")
}
}
fun setAttendanceList(list: List<Attendance>) {
_attendances.value = list
} }
/** /**
* ViewModel Factory. * ViewModel Factory.
*/ */
companion object { companion object {
@ -40,9 +77,7 @@ class SessionViewModel(
override fun <T : ViewModel> create( override fun <T : ViewModel> create(
modelClass: Class<T> modelClass: Class<T>
): T { ): T {
return SessionViewModel( return SessionViewModel() as T
AttendanceRepository()
) as T
} }
} }
} }

View file

@ -0,0 +1,69 @@
package com.example.palto.ui.session.new_student
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.NfcViewModel
import com.example.palto.R
import com.example.palto.databinding.FragmentNewSessionBinding
import com.example.palto.databinding.FragmentNewStudentBinding
import com.example.palto.ui.CardViewModel
import com.example.palto.ui.UserViewModel
import com.example.palto.ui.menu.MenuViewModel
import com.example.palto.ui.session.SessionViewModel
/**
*
*/
class NewStudentFragment : Fragment() {
private val newStudentViewModel: NewStudentViewModel by viewModels()
private val sessionViewModel: SessionViewModel by navGraphViewModels(R.id.nav_graph)
private val userViewModel: UserViewModel by navGraphViewModels(R.id.nav_graph)
private val cardViewModel: CardViewModel by navGraphViewModels(R.id.nav_graph)
// This property is only valid between onCreateView and onDestroyView
private lateinit var binding: FragmentNewStudentBinding
@OptIn(ExperimentalStdlibApi::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentNewStudentBinding.inflate(inflater, container, false)
// If the bundle value tagId of key "tagId" exists,
// Set it on the screen.
val tagId = arguments?.getString("tagId")
if (tagId != null) {
binding.newStudentCardId.text = tagId
}
// Bind the create button.
binding.newStudentCreate.setOnClickListener {
val user = userViewModel.createUser(binding.newStudentName.text.toString())
// If a tag has been provided, create the card.
// The user would not need to create his account afterward.
if (tagId != null) {
cardViewModel.createCard(user, tagId)
}
sessionViewModel.addAttendance(user)
findNavController().popBackStack()
}
return binding.root
}
}

View file

@ -0,0 +1,8 @@
package com.example.palto.ui.session.new_student
import androidx.lifecycle.ViewModel
/**
* ViewModel of the session creation form. Used for verifications.
*/
class NewStudentViewModel() : ViewModel()

View file

@ -0,0 +1,44 @@
<?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="match_parent">
<EditText
android:id="@+id/new_student_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/new_student_name"
android:inputType="text"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@+id/new_student_card_id"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/new_student_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_student_card_id_text"
app:layout_constraintBottom_toTopOf="@+id/new_student_create"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_student_name" />
<Button
android:id="@+id/new_student_create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_student_button_create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_student_card_id" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -5,7 +5,14 @@
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/card_̤id" android:id="@+id/attendance_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="@+id/attendance_date"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin" android:layout_margin="@dimen/text_margin"

View file

@ -17,10 +17,11 @@
tools:listitem="@layout/fragment_session_item" /> tools:listitem="@layout/fragment_session_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/create_card" android:id="@+id/add_student"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true" android:clickable="true"
android:contentDescription="@string/create_card" android:contentDescription="@string/create_card"
app:srcCompat="@android:drawable/ic_input_add" /> app:srcCompat="@android:drawable/ic_input_add" />

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <!--<item
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:title="@string/menu_item_logout" /> android:title="@string/menu_item_logout" />-->
</menu> </menu>

View file

@ -34,7 +34,16 @@
android:id="@+id/sessionFragment" android:id="@+id/sessionFragment"
android:name="com.example.palto.ui.session.SessionFragment" android:name="com.example.palto.ui.session.SessionFragment"
android:label="Fiche de présence" android:label="Fiche de présence"
tools:layout="@layout/fragment_session_list" /> tools:layout="@layout/fragment_session_list" >
<action
android:id="@+id/action_sessionFragment_to_newStudentFragment"
app:destination="@id/newStudentFragment" />
</fragment>
<fragment
android:id="@+id/newStudentFragment"
android:name="com.example.palto.ui.session.new_student.NewStudentFragment"
android:label="Nouvel étudiant"
tools:layout="@layout/fragment_new_student" />
</navigation> </navigation>

View file

@ -19,4 +19,9 @@
<string name="new_session_name_hint">Nom de la fiche</string> <string name="new_session_name_hint">Nom de la fiche</string>
<string name="new_session_button_create">Créer</string> <string name="new_session_button_create">Créer</string>
<string name="menu_item_logout">Déconnexion</string> <string name="menu_item_logout">Déconnexion</string>
<string name="new_student_button_create">Ajouter</string>
<string name="new_student_name">Nom de létudiant</string>
<string name="new_student_card_id_text">ID Carte : Pas de carte</string>
<string name="session_user_already_added">Létudiant %1$s existe déjà dans la liste !\n</string>
<string name="session_user_added">Létudiant %1$s a été ajouté</string>
</resources> </resources>