cards #8

Merged
faraphel merged 12 commits from cards into main 2024-06-13 15:12:13 +02:00
16 changed files with 234 additions and 57 deletions
Showing only changes of commit 7545e6aca9 - Show all commits

View file

@ -3,8 +3,8 @@ package com.faraphel.tasks_valider.connectivity.task
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
/**
@ -19,19 +19,21 @@ class TaskClient(
private val baseCookies: List<okhttp3.Cookie> = listOf()
) {
private val baseUrl = "http://$address:$port"
private val client = OkHttpClient().newBuilder().cookieJar(
// TODO(Faraphel): should be moved into another object
object : okhttp3.CookieJar {
private val cookies = baseCookies.toMutableList() ///< list of cookies
private val client = OkHttpClient().newBuilder()
.cookieJar(
// TODO(Faraphel): should be moved into another object
object : okhttp3.CookieJar {
private val cookies = baseCookies.toMutableList() ///< list of cookies
override fun loadForRequest(url: HttpUrl): List<okhttp3.Cookie> {
return this.cookies
override fun loadForRequest(url: HttpUrl): List<okhttp3.Cookie> {
return this.cookies
}
override fun saveFromResponse(url: HttpUrl, cookies: List<okhttp3.Cookie>) {
this.cookies.addAll(cookies)
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<okhttp3.Cookie>) {
this.cookies.addAll(cookies)
}
}
).build()
)
.build()
// TODO(Faraphel): automatically convert content to the correct type ?
@ -83,7 +85,7 @@ class TaskClient(
this.client.newCall(
this.baseRequestBuilder(endpoint)
.patch(content.toRequestBody(type.toMediaType()))
.build()
.build()
).execute()
/**
@ -96,6 +98,6 @@ class TaskClient(
this.client.newCall(
this.baseRequestBuilder(endpoint)
.delete(content.toRequestBody(type.toMediaType()))
.build()
.build()
).execute()
}

View file

@ -4,6 +4,11 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken
class ClassApi(dao: BaseTaskDao<ClassEntity>, session: SessionEntity) :
BaseTaskApi<ClassEntity>(dao, session)
BaseTaskApi<ClassEntity>(
dao,
object: TypeToken<ClassEntity>() {},
session
)

View file

@ -4,6 +4,11 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken
class PersonApi(dao: BaseTaskDao<PersonEntity>, session: SessionEntity) :
BaseTaskApi<PersonEntity>(dao, session)
BaseTaskApi<PersonEntity>(
dao,
object: TypeToken<PersonEntity>() {},
session
)

View file

@ -4,7 +4,12 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken
class RelationClassPersonApi(dao: BaseTaskDao<RelationClassPersonEntity>, session: SessionEntity) :
BaseTaskApi<RelationClassPersonEntity>(dao, session)
BaseTaskApi<RelationClassPersonEntity>(
dao,
object: TypeToken<RelationClassPersonEntity>() {},
session
)

View file

@ -4,7 +4,12 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken
class RelationPersonSessionSubjectApi(dao: BaseTaskDao<RelationPersonSessionSubjectEntity>, session: SessionEntity) :
BaseTaskApi<RelationPersonSessionSubjectEntity>(dao, session)
BaseTaskApi<RelationPersonSessionSubjectEntity>(
dao,
object: TypeToken<RelationPersonSessionSubjectEntity>() {},
session
)

View file

@ -3,6 +3,11 @@ package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken
class SessionApi(dao: BaseTaskDao<SessionEntity>, session: SessionEntity) :
BaseTaskApi<SessionEntity>(dao, session)
BaseTaskApi<SessionEntity>(
dao,
object: TypeToken<SessionEntity>() {},
session
)

View file

@ -4,7 +4,12 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.SubjectEntity
import com.google.gson.reflect.TypeToken
class SubjectApi(dao: BaseTaskDao<SubjectEntity>, session: SessionEntity) :
BaseTaskApi<SubjectEntity>(dao, session)
BaseTaskApi<SubjectEntity>(
dao,
object: TypeToken<SubjectEntity>() {},
session
)

View file

@ -4,7 +4,12 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.TaskEntity
import com.google.gson.reflect.TypeToken
class TaskApi(dao: BaseTaskDao<TaskEntity>, session: SessionEntity) :
BaseTaskApi<TaskEntity>(dao, session)
BaseTaskApi<TaskEntity>(
dao,
object: TypeToken<TaskEntity>() {},
session
)

View file

@ -4,7 +4,12 @@ import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.ValidationEntity
import com.google.gson.reflect.TypeToken
class ValidationApi(dao: BaseTaskDao<ValidationEntity>, session: SessionEntity) :
BaseTaskApi<ValidationEntity>(dao, session)
BaseTaskApi<ValidationEntity>(
dao,
object: TypeToken<ValidationEntity>() {},
session
)

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.database.api.entities.base
import android.util.Log
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.Gson
@ -11,13 +12,14 @@ import fi.iki.elonen.NanoHTTPD
* This is preconfigured to handle JSON data.
* @param Entity the entity type to handle
*/
abstract class BaseJsonApi<Entity: BaseEntity>(private val dao: BaseCronDao<Entity>) : BaseApi {
abstract class BaseJsonApi<Entity: BaseEntity>(
private val dao: BaseCronDao<Entity>,
private val entityTypeToken: TypeToken<Entity>,
) : BaseApi {
companion object {
private val parser = Gson() ///< The JSON parser
}
private val entityTypeToken: TypeToken<Entity> = object: TypeToken<Entity>() {} ///< the type of the managed entity
// Requests
override fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
@ -43,10 +45,16 @@ abstract class BaseJsonApi<Entity: BaseEntity>(private val dao: BaseCronDao<Enti
}
override fun post(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val data = httpSession.inputStream.bufferedReader().readText()
Log.i("post data", "data raw : $data")
val obj = parser.fromJson<Entity>(
httpSession.inputStream.bufferedReader().readText(),
data,
this.entityTypeToken.type
)
Log.i("post data", "data parsed : $obj")
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(

View file

@ -14,14 +14,16 @@ import fi.iki.elonen.NanoHTTPD
*/
abstract class BaseTaskApi<Entity: BaseEntity>(
private val dao: BaseTaskDao<Entity>,
private val entityTypeToken: TypeToken<Entity>,
private val session: SessionEntity,
) : BaseJsonApi<Entity>(dao) {
) : BaseJsonApi<Entity>(
dao,
entityTypeToken,
) {
companion object {
private val parser = Gson() ///< The JSON parser
}
private val entityTypeToken: TypeToken<Entity> = object: TypeToken<Entity>() {} ///< the type of the managed entity
// Requests
override fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {

View file

@ -1,6 +1,8 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
import android.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
@ -19,12 +21,14 @@ import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.task.TaskSessionScreen
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetClientScreen(activity: Activity) {
val client = remember { mutableStateOf<TaskClient?>(null) }
if (client.value == null) CommunicationInternetClientContent(client)
else TaskSessionScreen(activity, client.value!!)
// TODO(Faraphel): fix and get a user
// if (client.value == null) CommunicationInternetClientContent(client)
// else TaskSessionScreen(activity, client.value!!, user)
}

View file

@ -28,6 +28,7 @@ import com.faraphel.tasks_valider.ui.screen.authentification.AuthentificationSer
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.task.TaskSessionScreen
import kotlinx.coroutines.flow.MutableStateFlow
import java.time.Instant
@ -42,26 +43,32 @@ fun CommunicationInternetServerScreen(
) {
val controller = rememberNavController()
val adminPersonEntityRaw = remember { mutableStateOf<PersonEntity?>(null) }
val adminPersonEntity = remember { mutableStateOf<PersonEntity?>(null) }
val client = remember { mutableStateOf<TaskClient?>(null) }
NavHost(navController = controller, startDestination = "authentication") {
composable("authentication") {
// if the admin person is not created, prompt the user for the admin information
if (adminPersonEntity.value == null) AuthentificationServerScreen(adminPersonEntity)
if (adminPersonEntityRaw.value == null) AuthentificationServerScreen(adminPersonEntityRaw)
else controller.navigate("configuration")
}
composable("configuration") {
if (client.value == null)
CommunicationInternetServerContent(
database,
adminPersonEntity.value!!,
client
adminPersonEntityRaw,
adminPersonEntity,
client,
)
else controller.navigate("session")
}
composable("session") {
TaskSessionScreen(activity, client.value!!)
TaskSessionScreen(
activity,
client.value!!,
adminPersonEntity.value!!
)
}
}
}
@ -71,7 +78,8 @@ fun CommunicationInternetServerScreen(
@Composable
fun CommunicationInternetServerContent(
database: TaskDatabase,
adminPersonEntity: PersonEntity,
adminPersonEntityRaw: MutableState<PersonEntity?>,
adminPersonEntity: MutableState<PersonEntity?>,
client: MutableState<TaskClient?>
) {
val classes = remember { mutableStateOf<List<ClassEntity>?>(null) }
@ -129,16 +137,17 @@ fun CommunicationInternetServerContent(
Button(onClick = {
Thread { // a thread is used for networking
// Insert the admin in the database
database.personDao().insert(adminPersonEntity)
// Insert the admin in the database and get its id
val (adminPersonEntityId) = database.personDao().insert(adminPersonEntityRaw.value!!)
adminPersonEntity.value = database.personDao().getById(adminPersonEntityId)!!
// Create a new session
// TODO(Faraphel): name
val (sessionId) = database.sessionDao().insert(
SessionEntity(
name="NOM",
start=Instant.now(),
classId=selectedClass.value!!.id,
name="NOM",
start=Instant.now(),
classId=selectedClass.value!!.id,
)
)
val session = database.sessionDao().getById(sessionId)!!
@ -157,7 +166,7 @@ fun CommunicationInternetServerContent(
serverPort.intValue,
database,
session,
adminPersonEntity,
adminPersonEntity.value!!,
)
server.start()

View file

@ -30,10 +30,11 @@ import com.faraphel.tasks_valider.ui.screen.task.TaskSessionScreen
fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager) {
val client = remember { mutableStateOf<TaskClient?>(null) }
// TODO(Faraphel): fix and get a user
// if the server is not created, prompt the user for the server configuration
if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client)
// if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client)
// else, go to the base tasks screen
else TaskSessionScreen(activity, client.value!!)
// else TaskSessionScreen(activity, client.value!!)
}

View file

@ -1,7 +1,9 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
@ -20,8 +22,13 @@ import com.google.gson.reflect.TypeToken
* @param activity the android activity
* @param client an HTTP client that can communicate with the server
*/
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun TaskSessionScreen(activity: Activity, client: TaskClient) {
fun TaskSessionScreen(
activity: Activity,
client: TaskClient,
user: PersonEntity,
) {
val students = remember { mutableStateOf<List<PersonEntity>?>(null) }
val selectedStudent = remember { mutableStateOf<PersonEntity?>(null) }
@ -30,7 +37,12 @@ fun TaskSessionScreen(activity: Activity, client: TaskClient) {
return Thread { refreshStudents(activity, client, students) }.start()
if (selectedStudent.value != null)
return TaskStudentScreen(activity, client, selectedStudent.value!!)
return TaskStudentScreen(
activity,
client,
user,
selectedStudent.value!!
)
Column {
// if the groups have already been defined, display them
@ -44,7 +56,11 @@ fun TaskSessionScreen(activity: Activity, client: TaskClient) {
// TODO(Faraphel): template this function ?
fun refreshStudents(activity: Activity, client: TaskClient, students: MutableState<List<PersonEntity>?>) {
fun refreshStudents(
activity: Activity,
client: TaskClient,
students: MutableState<List<PersonEntity>?>
) {
// TODO(Faraphel): global variable ?
val jsonParser = Gson()

View file

@ -1,49 +1,103 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.database.entities.*
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.time.Instant
/**
* This screen represent a student
* @param student the student object
*/
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun TaskStudentScreen(activity: Activity, client: TaskClient, student: PersonEntity) {
fun TaskStudentScreen(
activity: Activity,
client: TaskClient,
user: PersonEntity,
student: PersonEntity,
) {
val tasks = remember { mutableStateOf<List<TaskEntity>?>(null) }
val validations = remember { mutableStateOf<List<ValidationEntity>?>(null) }
if (tasks.value == null)
Thread { refreshTasks(activity, client, student, tasks) }.start()
if (tasks.value == null || validations.value == null)
Thread {
refreshTasksValidations(
activity,
client,
student,
tasks,
validations
)
}.start()
Column {
Text(text = student.fullName())
tasks.value?.forEach { task ->
Button(onClick = {}) {
Column {
Text(task.title)
task.description?.let { description -> Text(description) }
// if both the list of tasks and validations are loaded
if (!(tasks.value == null || validations.value == null)) {
tasks.value?.forEach { task ->
// get the validation
val validation = validations.value!!.firstOrNull { validation -> validation.taskId == task.id }
Button(onClick = {}) {
Row {
Column {
// task title
Text(task.title)
// task description
task.description?.let { description -> Text(description) }
// if the task have been validated, show the date
if (validation != null) Text(validation.date.toString())
}
// the validation state
Checkbox(
checked = validation != null,
enabled = user.role.permissions.contains(TaskPermission.WRITE),
onCheckedChange = { state ->
Thread {
updateValidation(
client,
state,
validation ?: ValidationEntity(
Instant.now(),
user.id,
student.id,
task.id,
)
)
}.start()
}
)
}
}
}
}
}
}
fun refreshTasks(
fun refreshTasksValidations(
activity: Activity,
client: TaskClient,
student: PersonEntity,
tasks: MutableState<List<TaskEntity>?>
tasks: MutableState<List<TaskEntity>?>,
validations: MutableState<List<ValidationEntity>?>,
) {
val jsonParser = Gson()
@ -86,4 +140,45 @@ fun refreshTasks(
}.sortedBy { task ->
task.order
}
// try to obtain the list of validations
val responseValidations = client.get("entities/" + ValidationEntity.TABLE_NAME)
// in case of error, notify it
if (!responseValidations.isSuccessful)
return activity.runOnUiThread { Toast.makeText(activity, responseTasks.message, Toast.LENGTH_LONG).show() }
// parse the list of validations
val allValidations = jsonParser.fromJson<List<ValidationEntity>>(
responseValidations.body.string(),
object : TypeToken<List<ValidationEntity>>(){}.type
)
// filter only the interesting validations
validations.value = allValidations.filter { validation ->
validation.studentId == student.id &&
validation.taskId in allTasks.map { task -> task.id }
}
}
fun updateValidation(client: TaskClient, checked: Boolean, validation: ValidationEntity) {
val jsonParser = Gson()
if (checked) {
// if the validation is not set, create it
client.post(
"entities/" + ValidationEntity.TABLE_NAME,
jsonParser.toJson(validation),
"application/json"
)
}
else {
// if the validation is set, delete it
client.delete(
"entities/" + ValidationEntity.TABLE_NAME,
jsonParser.toJson(validation),
"application/json"
)
}
}