cards #8

Merged
faraphel merged 12 commits from cards into main 2024-06-13 15:12:13 +02:00
93 changed files with 2391 additions and 699 deletions

View file

@ -11,7 +11,7 @@ android {
defaultConfig {
applicationId = "com.faraphel.tasks_valider"
minSdk = 24
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
@ -52,8 +52,8 @@ android {
}
dependencies {
implementation("androidx.core:core-ktx:1.13.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui")
@ -66,6 +66,8 @@ dependencies {
implementation("org.nanohttpd:nanohttpd:2.3.1")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.okhttp3:okhttp-android:5.0.0-alpha.14")
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
implementation("com.google.zxing:core:3.5.3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
@ -74,4 +76,5 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
ksp("androidx.room:room-compiler:2.6.1")
implementation(kotlin("reflect"))
}

View file

@ -5,12 +5,12 @@
<!-- SDK -->
<uses-sdk
android:minSdkVersion="24"
android:minSdkVersion="26"
tools:ignore="GradleOverrides" />
<!-- Permissions -->
<!-- Internet -->
<!-- Permissions: Internet -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
@ -26,6 +26,10 @@
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<!-- Permissions: Scan -->
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Applications -->
<!-- NOTE: usesCleartextTraffic is enabled because of the API system using simple HTTP -->
@ -47,9 +51,12 @@
android:theme="@style/Theme.Tasksvalider">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.google.zxing.client.android.SCAN"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>

View file

@ -6,24 +6,41 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import androidx.room.Room
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.CommunicationScreen
import com.faraphel.tasks_valider.database.populateTaskDatabaseTest
import com.faraphel.tasks_valider.ui.screen.communication.CommunicationModeSelectionScreen
class MainActivity : ComponentActivity() {
private var bwfManager: BwfManager? = null ///< the WiFi-Direct helper
companion object {
private var bwdManager: BwdManager? = null ///< the WiFi-Direct helper
private lateinit var database: TaskDatabase ///< the database manager
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Reset the database | TODO(Faraphel): only for testing purpose
this.deleteDatabase("local")
// Create the database
this.database = Room.databaseBuilder(
this,
TaskDatabase::class.java,
"local"
).build()
// Populate the database with test data
// TODO(Faraphel): remove test data
Thread {
populateTaskDatabaseTest(database)
}.let { thread ->
thread.start()
thread.join()
}
this.setContent {
CommunicationScreen(this)
CommunicationModeSelectionScreen(this, database)
}
}
@ -32,13 +49,13 @@ class MainActivity : ComponentActivity() {
super.onResume()
// enable the WiFi-Direct events
this.registerReceiver(this.bwfManager, BwfManager.ALL_INTENT_FILTER)
this.registerReceiver(this.bwdManager, BwdManager.ALL_INTENT_FILTER)
}
override fun onPause() {
super.onPause()
// disable the WiFi-Direct events
this.unregisterReceiver(this.bwfManager)
this.unregisterReceiver(this.bwdManager)
}
}

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.connectivity.bwf
package com.faraphel.tasks_valider.connectivity.bwd
import android.Manifest
import android.app.Activity
@ -10,7 +10,7 @@ import android.content.pm.PackageManager
import android.net.wifi.p2p.*
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import com.faraphel.tasks_valider.connectivity.bwf.error.*
import com.faraphel.tasks_valider.connectivity.bwd.error.*
/**
@ -22,7 +22,7 @@ import com.faraphel.tasks_valider.connectivity.bwf.error.*
* @param manager The WiFi-Direct manager
* @param channel The WiFi-Direct channel
*/
class BwfManager(
class BwdManager(
private var manager: WifiP2pManager,
private var channel: WifiP2pManager.Channel,
) : BroadcastReceiver() {
@ -41,11 +41,11 @@ class BwfManager(
* Create a new BwfManager from an activity.
* @param activity The activity to create the manager from
*/
fun fromActivity(activity: Activity): BwfManager {
fun fromActivity(activity: Activity): BwdManager {
// check if the system support WiFi-Direct
if (this.isSupported(activity)) {
Log.e("wifi-p2p", "this device does not support the WiFi-Direct feature")
throw BwfNotSupportedException()
throw BwdNotSupportedException()
}
// TODO(Faraphel): more check on permissions
@ -62,11 +62,11 @@ class BwfManager(
// get the WiFi-Direct manager
val manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
?: throw BwfPermissionException()
?: throw BwdPermissionException()
// get the WiFi-Direct channel
val channel = manager.initialize(activity, activity.mainLooper, null)
return BwfManager(manager, channel)
return BwdManager(manager, channel)
// NOTE(Faraphel): the broadcast receiver should be registered in the activity onResume
}
@ -83,7 +83,7 @@ class BwfManager(
fun connect(config: WifiP2pConfig, callback: () -> Unit = {}) =
this.manager.connect(this.channel, config, object : WifiP2pManager.ActionListener {
override fun onSuccess() { callback() }
override fun onFailure(reason: Int) = throw BwfConnectException(reason)
override fun onFailure(reason: Int) = throw BwdConnectException(reason)
})
/**
@ -105,7 +105,7 @@ class BwfManager(
fun discoverPeers(callback: () -> Unit = {}) =
this.manager.discoverPeers(this.channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() { callback() }
override fun onFailure(reason: Int) = throw BwfDiscoverException(reason)
override fun onFailure(reason: Int) = throw BwdDiscoverException(reason)
})
/**
@ -133,7 +133,7 @@ class BwfManager(
fun createGroup(callback: () -> Unit = {}) =
this.manager.createGroup(this.channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() { callback() }
override fun onFailure(reason: Int) = throw BwfCreateGroupException(reason)
override fun onFailure(reason: Int) = throw BwdCreateGroupException(reason)
})
/**
@ -143,7 +143,7 @@ class BwfManager(
fun removeGroup(callback: () -> Unit = {}) =
this.manager.removeGroup(this.channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() { callback() }
override fun onFailure(reason: Int) = throw BwfRemoveGroupException(reason)
override fun onFailure(reason: Int) = throw BwdRemoveGroupException(reason)
})
/**
@ -161,7 +161,7 @@ class BwfManager(
this.requestGroupInfo { group ->
// if a group exist, quit it
if (group != null)
this.removeGroup { this@BwfManager.createGroup(callback) }
this.removeGroup { this@BwdManager.createGroup(callback) }
else
// create the group
this.createGroup(callback)

View file

@ -0,0 +1,5 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdConnectException(
reason: Int
) : BwdException("Cannot connect to the peer. Reason: $reason")

View file

@ -0,0 +1,5 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdCreateGroupException (
reason: Int
) : BwdException("Could not create the group : $reason")

View file

@ -0,0 +1,5 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdDiscoverException(
reason: Int
) : BwdException("Could not discover peers : $reason")

View file

@ -1,9 +1,9 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
package com.faraphel.tasks_valider.connectivity.bwd.error
/**
* Base Exception for everything concerning the WifiP2pHelper class
*/
open class BwfException(
open class BwdException(
override val message: String?
) : Exception(message)

View file

@ -0,0 +1,5 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdInvalidActionException(
action: String
) : BwdException("This WiFi-Direct action is not supported : $action")

View file

@ -0,0 +1,4 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdNotSupportedException :
BwdException("WiFi-Direct is not supported on this device.")

View file

@ -0,0 +1,4 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdPermissionException :
BwdException("WiFi-Direct requires permissions to work properly. Please grant the permissions.")

View file

@ -0,0 +1,5 @@
package com.faraphel.tasks_valider.connectivity.bwd.error
class BwdRemoveGroupException (
reason: Int
) : BwdException("Could not remove the group : $reason")

View file

@ -1,5 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfConnectException(
reason: Int
) : BwfException("Cannot connect to the peer. Reason: $reason")

View file

@ -1,5 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfCreateGroupException (
reason: Int
) : BwfException("Could not create the group : $reason")

View file

@ -1,5 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfDiscoverException(
reason: Int
) : BwfException("Could not discover peers : $reason")

View file

@ -1,5 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfInvalidActionException(
action: String
) : BwfException("This WiFi-Direct action is not supported : $action")

View file

@ -1,4 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfNotSupportedException :
BwfException("WiFi-Direct is not supported on this device.")

View file

@ -1,4 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfPermissionException :
BwfException("WiFi-Direct requires permissions to work properly. Please grant the permissions.")

View file

@ -1,5 +0,0 @@
package com.faraphel.tasks_valider.connectivity.bwf.error
class BwfRemoveGroupException (
reason: Int
) : BwfException("Could not remove the group : $reason")

View file

@ -1,13 +1,11 @@
package com.faraphel.tasks_valider.connectivity.task
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.*
/**
* A client to handle the room connection.
* A client to handle the room connection and access the API
* @param address the address of the server
* @param port the port of the server
* @param baseCookies list of cookies to use (optional)
@ -17,84 +15,15 @@ class TaskClient(
private val port: Int,
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 httpClient = TaskEntityHttpClient(address, port, baseCookies)
override fun loadForRequest(url: HttpUrl): List<okhttp3.Cookie> {
return this.cookies
}
override fun saveFromResponse(url: HttpUrl, cookies: List<okhttp3.Cookie>) {
this.cookies.addAll(cookies)
}
}
).build()
// TODO(Faraphel): automatically convert content to the correct type ?
/**
* Return a basic request to the server
* @param endpoint the endpoint of the server
*/
private fun baseRequestBuilder(endpoint: String): okhttp3.Request.Builder =
okhttp3.Request.Builder().url("$baseUrl/$endpoint")
/**
* Run a HEAD request
* @param endpoint the endpoint of the server
*/
fun head(endpoint: String): okhttp3.Request =
this.baseRequestBuilder(endpoint).head().build()
/**
* Run a GET request
* @param endpoint the endpoint of the server
*/
fun get(endpoint: String): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.get()
.build()
).execute()
/**
* Run a POST request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun post(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.post(content.toRequestBody(type.toMediaType()))
.build()
).execute()
/**
* Run a PATCH request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun patch(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.patch(content.toRequestBody(type.toMediaType()))
.build()
).execute()
/**
* Run a DELETE request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun delete(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.delete(content.toRequestBody(type.toMediaType()))
.build()
).execute()
val clientApi = ClassClientApi(httpClient)
val personApi = PersonClientApi(httpClient)
val sessionApi = SessionClientApi(httpClient)
val subjectApi = SubjectClientApi(httpClient)
val taskApi = TaskClientApi(httpClient)
val validationApi = ValidationClientApi(httpClient)
val relationClassPersonApi = RelationClassPersonClientApi(httpClient)
val relationPersonSessionSubjectApi = RelationPersonSessionSubjectClientApi(httpClient)
}

View file

@ -1,45 +1,51 @@
package com.faraphel.tasks_valider.connectivity.task
import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerApi
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.TaskDatabaseApi
import com.faraphel.tasks_valider.database.api.server.DatabaseApi
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import fi.iki.elonen.NanoHTTPD
/**
* A server to handle the task API to allow clients to interact with the database.
* @param port the port of the server
* @param database the database to interact with
* @param port the port of the server.
* @param database the database to interact with.
* @param session the current session.
* @param adminPersonEntity the person that represent the host of the session.
*/
class TaskServer(
private val port: Int,
private val database: TaskDatabase
private val database: TaskDatabase,
private val session: SessionEntity,
private val adminPersonEntity: PersonEntity,
) : NanoHTTPD(port) {
companion object {
private val TASK_SESSION_ADMIN = TaskSession( ///< the admin default session
role = TaskRole.ADMIN
)
private val sessionManager = TaskSessionManager(adminPersonEntity) ///< the session manager
private val databaseApi = DatabaseApi(this.database, session) ///< the api of the database
private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager, this.database) ///< the api of the session manager
/**
* Get the admin person entity
* @return the admin person entity
*/
fun getAdminPersonEntity(): PersonEntity {
return this.sessionManager.getAdminPersonEntity()
}
private val sessionManager = TaskSessionManager() ///< the session manager
private val adminSessionId = this.sessionManager.newSessionData(TASK_SESSION_ADMIN) ///< default admin session id
private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager) ///< the api of the session manager
private val databaseApi = TaskDatabaseApi(this.database) ///< the api of the database
/**
* Return a new client that can be used by the admin
* @return the client
*/
fun getClientAdmin(): TaskClient {
fun getAdminClient(): TaskClient {
// create the session cookie for the admin
val cookieSession = okhttp3.Cookie.Builder()
.domain("localhost")
.name("sessionId")
.value(adminSessionId)
.name("sessionToken")
.value(this.sessionManager.adminSessionToken)
.build()
// create a new client
return TaskClient(
"localhost",
@ -54,7 +60,8 @@ class TaskServer(
*/
override fun serve(httpSession: IHTTPSession): Response {
// get the session data of the client
val taskSession = this.sessionManager.getOrCreateSessionData(httpSession)
val taskSessionData = this.sessionManager.getSessionData(httpSession)
val taskSession = taskSessionData?.second
// parse the url
val uri: String = httpSession.uri.trim('/')
@ -69,11 +76,11 @@ class TaskServer(
)
// get the response from the correct part of the application
val response = when (requestType) {
return when (requestType) {
// session requests
"sessions" -> this.sessionManagerApi.handleRequest(taskSession, httpSession, path)
// entities requests
"entities" -> return this.databaseApi.handleRequest(taskSession, httpSession, path)
"entities" -> this.databaseApi.handleRequest(taskSession, httpSession, path)
// invalid requests
else ->
newFixedLengthResponse(
@ -82,9 +89,6 @@ class TaskServer(
"Unknown request type"
)
}
// wrap additional information in the response
return this.sessionManager.responseSetSessionData(response, httpSession.cookies)
}
/**

View file

@ -1,10 +1,10 @@
package com.faraphel.tasks_valider.connectivity.task.api
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.google.gson.Gson
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.utils.parser
import com.google.gson.reflect.TypeToken
import fi.iki.elonen.NanoHTTPD
@ -12,9 +12,10 @@ import fi.iki.elonen.NanoHTTPD
/**
* the HTTP API for the session manager
*/
class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) {
private val jsonParser = Gson() ///< the json parser
class TaskSessionManagerApi(
private val sessionManager: TaskSessionManager,
private val database: TaskDatabase
) {
/**
* Handle a HTTP Api request
* @param taskSession the data of the client session
@ -22,109 +23,118 @@ class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) {
* @param path the path of the request
*/
fun handleRequest(
taskSession: TaskSession,
taskSession: TaskSession?,
httpSession: NanoHTTPD.IHTTPSession,
path: MutableList<String>,
): NanoHTTPD.Response {
// get the target session id
val targetSessionId = path.removeFirstOrNull()
val action = path.removeFirstOrNull()
return if (targetSessionId == null) {
// no specific session targeted
this.handleRequestGeneric(taskSession, httpSession)
} else {
// a specific session is targeted
this.handleRequestSpecific(taskSession, httpSession, targetSessionId)
return when (action) {
"self" -> this.handleRequestSelf(taskSession, httpSession, path)
"all" -> this.handleRequestAll(taskSession, httpSession, path)
else ->
NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Unknown action"
)
}
}
/**
* Handle a request with no specific session targeted
* Handle an HTTP Api request about the user own session
*/
private fun handleRequestGeneric(
taskSession: TaskSession,
fun handleRequestSelf(
taskSession: TaskSession?,
httpSession: NanoHTTPD.IHTTPSession,
path: MutableList<String>,
): NanoHTTPD.Response {
when (httpSession.method) {
// get all the session data
// get the session data
NanoHTTPD.Method.GET -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
// check if the session is valid
if (taskSession == null)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
NanoHTTPD.Response.Status.UNAUTHORIZED,
"text/plain",
"Forbidden"
"No session"
)
// return the session data
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
jsonParser.toJson(taskSession)
parser.toJson(taskSession)
)
}
// other action are limited
else -> {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Unknown method"
)
}
}
}
private fun handleRequestSpecific(
taskSession: TaskSession,
httpSession: NanoHTTPD.IHTTPSession,
targetSessionId: String,
): NanoHTTPD.Response {
when (httpSession.method) {
// change a specific client session data
// connect the user to the session
NanoHTTPD.Method.POST -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.ADMIN))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"You are not allowed to update a session"
)
// parse the content of the request
val targetSession = jsonParser.fromJson(
// get the user identifiers
val identifiers: Map<String, String> = parser.fromJson(
httpSession.inputStream.bufferedReader().readText(),
TaskSession::class.java
object : TypeToken<Map<String, String>>() {}.type
)
// update the session
this.sessionManager.setSessionData(targetSessionId, targetSession)
// success message
// check for the id
if (!identifiers.contains("id"))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Missing id"
)
// get the id of the user (if invalid, return an error)
val personId: Long = identifiers["id"]!!.toLongOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Invalid id"
)
// check for the password
if (!identifiers.contains("password"))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Missing password"
)
// check if the identifiers are correct
val person = this.database.personDao().getById(personId)
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"No person with this id"
)
// check if the password is correct
if (!person.checkPassword(identifiers["password"]!!))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.UNAUTHORIZED,
"text/plain",
"Invalid password"
)
// create a new session for the userJHH
val (sessionToken, session) = this.sessionManager.newSessionData(person)
// create the response
val response = NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session updated"
)
}
// delete the session
NanoHTTPD.Method.DELETE -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.ADMIN))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"You are not allowed to delete a session"
// set the session token in the cookies
this.sessionManager.responseSetSessionData(
response,
httpSession.cookies,
sessionToken
)
// delete the target session
this.sessionManager.deleteSessionData(targetSessionId)
// success message
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session deleted"
)
// return the response
return response
}
// ignore other methods
else -> {
@ -136,4 +146,86 @@ class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) {
}
}
}
/**
* Handle an HTTP Api request about all the sessions
*/
private fun handleRequestAll(
taskSession: TaskSession?,
httpSession: NanoHTTPD.IHTTPSession,
path: MutableList<String>,
): NanoHTTPD.Response {
// check if the session is valid
if (taskSession == null)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.UNAUTHORIZED,
"text/plain",
"No session"
)
// check the permission of the session
if (taskSession.person.role.permissions.contains(TaskPermission.ADMIN))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
"You are not allowed to update a session"
)
when (httpSession.method) {
// change a specific client session data
NanoHTTPD.Method.POST -> {
// parse the content of the request
val targetSession = parser.fromJson(
httpSession.inputStream.bufferedReader().readText(),
TaskSession::class.java
)
val targetSessionToken = path.removeFirstOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Missing session token"
)
// update the session
this.sessionManager.updateSessionData(targetSessionToken, targetSession)
// success message
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session updated"
)
}
// delete the session
NanoHTTPD.Method.DELETE -> {
val targetSessionToken = path.removeFirstOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Missing session token"
)
// delete the target session
this.sessionManager.deleteSessionData(targetSessionToken)
// success message
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session deleted"
)
}
// ignore other methods
else -> {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Invalid method"
)
}
}
}
}

View file

@ -1,13 +1,13 @@
package com.faraphel.tasks_valider.connectivity.task.session
import com.faraphel.tasks_valider.database.entities.PersonEntity
import kotlinx.serialization.Serializable
/**
* store the data of a session in the task system
* @param role the role accorded to the session
*/
@Serializable
data class TaskSession(
var role: TaskRole = TaskRole.STUDENT
val person: PersonEntity,
)

View file

@ -1,7 +1,6 @@
package com.faraphel.tasks_valider.connectivity.task.session
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.faraphel.tasks_valider.database.entities.PersonEntity
import fi.iki.elonen.NanoHTTPD
import java.util.*
@ -9,21 +8,37 @@ import java.util.*
/**
* The manager for the session system
*/
class TaskSessionManager {
class TaskSessionManager(///< the admin person entity
private val adminPersonEntity: PersonEntity
) {
private val sessions = mutableMapOf<String, TaskSession>() ///< sessions specific data
private val adminPersonData = this.newSessionData(this.adminPersonEntity) ///< the session of the admin
val adminSessionToken = this.adminPersonData.first ///< the session token of the admin
val adminSession = this.adminPersonData.second ///< the session of the admin
/**
* Get the admin person entity
* @return the admin person entity
*/
fun getAdminPersonEntity(): PersonEntity {
return this.adminPersonEntity
}
/**
* Create a new session
* @param session the data for the session (optional)
* @param sessionId the session id to use (optional)
* @return a new session identifier
* @param person the person for the session (optional)
* @return a new session identifier and the corresponding session
*/
fun newSessionData(
session: TaskSession = TaskSession(),
sessionId: String = UUID.randomUUID().toString()
): String {
this.sessions[sessionId] = session
return sessionId
fun newSessionData(person: PersonEntity): Pair<String, TaskSession> {
// create a new session token that is a secret random string
val sessionToken: String = UUID.randomUUID().toString()
// create a new session
val session = TaskSession(person)
// store the session
this.sessions[sessionToken] = session
// return the session data
return Pair(sessionToken, session)
}
/**
@ -31,54 +46,60 @@ class TaskSessionManager {
* @param httpSession the HTTP session
* @return the session data
*/
fun getSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession? {
val sessionId = httpSession.cookies.read("sessionId") ?: return null
val sessionData = this.getSessionData(sessionId)
fun getSessionData(httpSession: NanoHTTPD.IHTTPSession): Pair<String, TaskSession>? {
// get the session token from the cookies
val sessionToken = httpSession.cookies.read("sessionToken") ?: return null
// get the session data
val sessionData = this.getSessionData(sessionToken)
// return the session data
return sessionData
}
/**
* Get data from a session identifier
* @param sessionId the identifier of the session
* @param sessionToken the identifier of the session
* @return the session data
*/
fun getSessionData(sessionId: String): TaskSession? {
return this.sessions[sessionId]
fun getSessionData(sessionToken: String): Pair<String, TaskSession>? {
// get the session data
val session = this.sessions[sessionToken]
?: return null
// return the session data
return Pair(sessionToken, session)
}
/**
* Set the data of a session
* @param sessionId the identifier of the session
* @param session the session data
* Update the session data
* @param sessionToken the identifier of the session
* @param session the new session data
*/
fun setSessionData(sessionId: String, session: TaskSession) {
this.sessions[sessionId] = session
fun updateSessionData(sessionToken: String, session: TaskSession) {
// update the session
this.sessions[sessionToken] = session
}
/**
* Delete a session
* @param sessionId the identifier of the session
* @param sessionToken the identifier of the session
*/
fun deleteSessionData(sessionId: String): TaskSession? {
return this.sessions.remove(sessionId)
fun deleteSessionData(sessionToken: String): Pair<String, TaskSession>? {
// remove the session
val session = this.sessions.remove(sessionToken) ?:
return null
// return the session data
return Pair(sessionToken, session)
}
/**
* Get data from a http session. If it does not exist, create it.
* @param httpSession the HTTP session
*/
fun getOrCreateSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession {
fun getOrCreateSessionData(httpSession: NanoHTTPD.IHTTPSession, person: PersonEntity): Pair<String, TaskSession> {
// try to get the session directly
var session = this.getSessionData(httpSession)
// if the session does not exist, create it
if (session == null) {
val sessionId = this.newSessionData()
session = this.getSessionData(sessionId)!!
}
// return the session
return session
val sessionData = this.getSessionData(httpSession)
?: this.newSessionData(person)
// return the session data
return sessionData
}
/**
@ -88,10 +109,11 @@ class TaskSessionManager {
*/
fun responseSetSessionData(
response: NanoHTTPD.Response,
cookies: NanoHTTPD.CookieHandler
cookies: NanoHTTPD.CookieHandler,
sessionToken: String
): NanoHTTPD.Response {
// update the cookie of the user
cookies.set(NanoHTTPD.Cookie("sessionId", this.newSessionData()))
cookies.set(NanoHTTPD.Cookie("sessionToken", sessionToken))
// load them in the response
cookies.unloadQueue(response)

View file

@ -3,7 +3,7 @@ package com.faraphel.tasks_valider.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.faraphel.tasks_valider.database.converters.InstantConverter
import com.faraphel.tasks_valider.utils.converters.InstantConverter
import com.faraphel.tasks_valider.database.dao.*
import com.faraphel.tasks_valider.database.entities.*
@ -22,6 +22,7 @@ import com.faraphel.tasks_valider.database.entities.*
ValidationEntity::class,
RelationClassPersonEntity::class,
RelationPersonSessionSubjectEntity::class,
],
version = 1
)
@ -37,4 +38,5 @@ abstract class TaskDatabase: RoomDatabase() {
abstract fun validationDao(): ValidationDao
abstract fun relationClassPersonDao(): RelationClassPersonDao
abstract fun relationPersonSessionSubjectDao(): RelationPersonSessionSubjectDao
}

View file

@ -0,0 +1,98 @@
package com.faraphel.tasks_valider.database.api.client
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import kotlin.time.Duration.Companion.seconds
/**
* An HTTP client to handle the room connection.
* @param address the address of the server
* @param port the port of the server
* @param baseCookies list of cookies to use (optional)
*/
class TaskEntityHttpClient(
private val address: String,
private val port: Int,
private val baseCookies: List<okhttp3.Cookie> = listOf()
) {
// the base url for the server
private val baseUrl = "http://$address:$port"
// the HTTP client
private val client = OkHttpClient().newBuilder()
.cookieJar(
object : okhttp3.CookieJar {
private val cookies = baseCookies.toMutableList() ///< list of cookies
override fun loadForRequest(url: HttpUrl) = this.cookies
override fun saveFromResponse(url: HttpUrl, cookies: List<okhttp3.Cookie>) { this.cookies.addAll(cookies) }
}
)
.callTimeout(30.seconds)
.build()
/**
* Return a basic request to the server
* @param endpoint the endpoint of the server
*/
private fun baseRequestBuilder(endpoint: String): okhttp3.Request.Builder =
okhttp3.Request.Builder().url("$baseUrl/$endpoint")
/**
* Run a HEAD request
* @param endpoint the endpoint of the server
*/
fun head(endpoint: String): okhttp3.Request =
this.baseRequestBuilder(endpoint).head().build()
/**
* Run a GET request
* @param endpoint the endpoint of the server
*/
fun get(endpoint: String): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.get()
.build()
).execute()
/**
* Run a POST request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun post(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.post(content.toRequestBody(type.toMediaType()))
.build()
).execute()
/**
* Run a PATCH request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun patch(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.patch(content.toRequestBody(type.toMediaType()))
.build()
).execute()
/**
* Run a DELETE request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun delete(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.delete(content.toRequestBody(type.toMediaType()))
.build()
).execute()
}

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.ClassEntity
class ClassClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<ClassEntity>(
client,
ClassEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.PersonEntity
class PersonClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<PersonEntity>(
client,
PersonEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity
class RelationClassPersonClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<RelationClassPersonEntity>(
client,
RelationClassPersonEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
class RelationPersonSessionSubjectClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<RelationPersonSessionSubjectEntity>(
client,
RelationPersonSessionSubjectEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.SessionEntity
class SessionClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<SessionEntity>(
client,
SessionEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.SubjectEntity
class SubjectClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<SubjectEntity>(
client,
SubjectEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.TaskEntity
class TaskClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<TaskEntity>(
client,
TaskEntity::class
)

View file

@ -0,0 +1,13 @@
package com.faraphel.tasks_valider.database.api.client.entities
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi
import com.faraphel.tasks_valider.database.entities.ValidationEntity
class ValidationClientApi(
client: TaskEntityHttpClient,
) : BaseClientApi<ValidationEntity>(
client,
ValidationEntity::class
)

View file

@ -0,0 +1,100 @@
package com.faraphel.tasks_valider.database.api.client.entities.base;
import android.util.Log
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.faraphel.tasks_valider.database.entities.error.HttpException
import com.faraphel.tasks_valider.utils.parser
import com.google.gson.reflect.TypeToken
import kotlin.reflect.KClass
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.full.declaredMemberProperties
abstract class BaseClientApi<Entity: BaseEntity>(
private val client: TaskEntityHttpClient,
private val entityType: KClass<Entity>,
) {
/**
* return the API endpoint for this entity
* @return the API endpoint for this entity
*/
private fun getEndpoint(): String {
// get the property for the name of the table
val propertyTableName = entityType.companionObject!!.declaredMemberProperties.first { member ->
member.name == "TABLE_NAME"
}
// get the table name by calling the getter of the property
val tableName = propertyTableName.getter.call(entityType.companionObjectInstance)
// return the endpoint
return "entities/${tableName}"
}
/**
* return all the entities for that table
* @return all the entities for that table
* @throws java.io.IOException reading error while parsing request
* @throws HttpException error of the request
*/
fun getAll(): List<Entity> {
// try to obtain the list of validations
val response = client.get(this.getEndpoint())
// in case of error, notify it
if (!response.isSuccessful)
throw HttpException(response.code)
val data = response.body.string()
// parse the list of validations
return parser.fromJson(
data,
TypeToken.getParameterized(ArrayList::class.java, entityType.java).type
)
}
/**
* create a new entity in the table
* @return the id of the object in the database
* @throws java.io.IOException reading error while parsing request
* @throws HttpException error of the request
*/
fun save(entity: Entity): Long {
// try to send the serialized entity as json
val response = client.post(
this.getEndpoint(),
parser.toJson(entity),
"application/json; charset=utf-8"
)
// in case of error, notify it
if (!response.isSuccessful)
throw HttpException(response.code)
// return the id of the object
return response.body.string().toLong()
}
/**
* delete an entity in the table
* @return the number of object deleted in the database
* @throws java.io.IOException reading error while parsing request
* @throws HttpException error of the request
*/
fun delete(entity: Entity): Long {
// try to delete the object
val response = client.delete(
this.getEndpoint(),
parser.toJson(entity),
"application/json; charset=utf-8"
)
// in case of error, notify it
if (!response.isSuccessful)
throw HttpException(response.code)
// return the id of the object
return response.body.string().toLong()
}
}

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.ClassEntity
class ClassApi(dao: BaseDao<ClassEntity>) : BaseJsonApi<ClassEntity>(dao)

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.PersonEntity
class PersonApi(dao: BaseDao<PersonEntity>) : BaseJsonApi<PersonEntity>(dao)

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity
class RelationClassPersonApi(dao: BaseDao<RelationClassPersonEntity>) : BaseJsonApi<RelationClassPersonEntity>(dao)

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
class SessionApi(dao: BaseDao<SessionEntity>) : BaseJsonApi<SessionEntity>(dao)

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.SubjectEntity
class SubjectApi(dao: BaseDao<SubjectEntity>) : BaseJsonApi<SubjectEntity>(dao)

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.TaskEntity
class TaskApi(dao: BaseDao<TaskEntity>) : BaseJsonApi<TaskEntity>(dao)

View file

@ -1,7 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.ValidationEntity
class ValidationApi(dao: BaseDao<ValidationEntity>) : BaseJsonApi<ValidationEntity>(dao)

View file

@ -1,72 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities.base
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import fi.iki.elonen.NanoHTTPD
/**
* A base for the API to handle the database operations.
* This is preconfigured to handle JSON data.
* @param Entity the entity type to handle
*/
abstract class BaseJsonApi<Entity: BaseEntity>(private val dao: BaseDao<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(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val obj = parser.fromJson<Entity>(
session.inputStream.bufferedReader().readText(),
this.entityTypeToken.type
)
val exists = this.dao.exists(obj)
return NanoHTTPD.newFixedLengthResponse(
if (exists) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND,
"text/plain",
if (exists) "Exists" else "Not found"
)
}
override fun get(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAll())
)
}
override fun post(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val obj = parser.fromJson<Entity>(
session.inputStream.bufferedReader().readText(),
this.entityTypeToken.type
)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
override fun delete(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val obj = parser.fromJson<Entity>(
session.inputStream.bufferedReader().readText(),
this.entityTypeToken.type
)
val count = this.dao.delete(obj)
return NanoHTTPD.newFixedLengthResponse(
if (count > 0) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND,
"text/plain",
count.toString()
)
}
}

View file

@ -1,23 +1,28 @@
package com.faraphel.tasks_valider.database.api
package com.faraphel.tasks_valider.database.api.server
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.entities.*
import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseDatabaseApi
import com.faraphel.tasks_valider.database.api.server.entities.*
import com.faraphel.tasks_valider.database.entities.*
import fi.iki.elonen.NanoHTTPD
class TaskDatabaseApi(private val database: TaskDatabase) {
private val api: Map<String, BaseApi> = mapOf(
ClassEntity.TABLE_NAME to ClassApi(this.database.classDao()),
PersonEntity.TABLE_NAME to PersonApi(this.database.personDao()),
SessionEntity.TABLE_NAME to SessionApi(this.database.sessionDao()),
SubjectEntity.TABLE_NAME to SubjectApi(this.database.subjectDao()),
TaskEntity.TABLE_NAME to TaskApi(this.database.taskDao()),
ValidationEntity.TABLE_NAME to ValidationApi(this.database.validationDao()),
RelationClassPersonEntity.TABLE_NAME to RelationClassPersonApi(this.database.relationClassPersonDao()),
class DatabaseApi(
private val database: TaskDatabase,
private val session: SessionEntity,
) {
private val api: Map<String, BaseDatabaseApi> = mapOf(
ClassEntity.TABLE_NAME to ClassDatabaseApi(this.database.classDao(), session),
PersonEntity.TABLE_NAME to PersonDatabaseApi(this.database.personDao(), session),
SessionEntity.TABLE_NAME to SessionDatabaseApi(this.database.sessionDao(), session),
SubjectEntity.TABLE_NAME to SubjectDatabaseApi(this.database.subjectDao(), session),
TaskEntity.TABLE_NAME to TaskDatabaseApi(database.taskDao(), session),
ValidationEntity.TABLE_NAME to ValidationDatabaseApi(this.database.validationDao(), session),
RelationClassPersonEntity.TABLE_NAME to RelationClassPersonDatabaseApi(this.database.relationClassPersonDao(), session),
RelationPersonSessionSubjectEntity.TABLE_NAME to RelationPersonSessionSubjectDatabaseApi(this.database.relationPersonSessionSubjectDao(), session),
)
/**
@ -27,10 +32,18 @@ class TaskDatabaseApi(private val database: TaskDatabase) {
* @param path the path of the request
*/
fun handleRequest(
taskSession: TaskSession,
taskSession: TaskSession?,
httpSession: NanoHTTPD.IHTTPSession,
path: MutableList<String>
): NanoHTTPD.Response {
// check if the user is authenticated
if (taskSession == null)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.UNAUTHORIZED,
"text/plain",
"Unauthorized"
)
// get the entity name
val entityName = path.removeFirstOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
@ -53,7 +66,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) {
// TODO(Faraphel): should only be allowed to read data concerning the current class session
NanoHTTPD.Method.HEAD -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
if (!taskSession.person.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
@ -65,7 +78,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) {
// get the data from the database
NanoHTTPD.Method.GET -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.READ))
if (!taskSession.person.role.permissions.contains(TaskPermission.READ))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
@ -77,7 +90,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) {
// insert the data into the database
NanoHTTPD.Method.POST -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
if (!taskSession.person.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",
@ -89,7 +102,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) {
// delete the data from the database
NanoHTTPD.Method.DELETE -> {
// check the permission of the session
if (taskSession.role.permissions.contains(TaskPermission.WRITE))
if (!taskSession.person.role.permissions.contains(TaskPermission.WRITE))
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.FORBIDDEN,
"text/plain",

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class ClassDatabaseApi(
dao: BaseTaskDao<ClassEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<ClassEntity>(
dao,
session,
ClassEntity::class.java
)

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class PersonDatabaseApi(
dao: BaseTaskDao<PersonEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<PersonEntity>(
dao,
session,
PersonEntity::class.java
)

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class RelationClassPersonDatabaseApi(
dao: BaseTaskDao<RelationClassPersonEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<RelationClassPersonEntity>(
dao,
session,
RelationClassPersonEntity::class.java
)

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class RelationPersonSessionSubjectDatabaseApi(
dao: BaseTaskDao<RelationPersonSessionSubjectEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<RelationPersonSessionSubjectEntity>(
dao,
session,
RelationPersonSessionSubjectEntity::class.java
)

View file

@ -0,0 +1,15 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
class SessionDatabaseApi(
dao: BaseTaskDao<SessionEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<SessionEntity>(
dao,
session,
SessionEntity::class.java
)

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class SubjectDatabaseApi(
dao: BaseTaskDao<SubjectEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<SubjectEntity>(
dao,
session,
SubjectEntity::class.java
)

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class TaskDatabaseApi(
dao: BaseTaskDao<TaskEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<TaskEntity>(
dao,
session,
TaskEntity::class.java
)

View file

@ -0,0 +1,16 @@
package com.faraphel.tasks_valider.database.api.server.entities
import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi
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
class ValidationDatabaseApi(
dao: BaseTaskDao<ValidationEntity>,
session: SessionEntity
) : BaseTaskDatabaseApi<ValidationEntity>(
dao,
session,
ValidationEntity::class.java
)

View file

@ -1,32 +1,32 @@
package com.faraphel.tasks_valider.database.api.entities.base
package com.faraphel.tasks_valider.database.api.server.entities.base
import fi.iki.elonen.NanoHTTPD
/**
* A base for the API to handle the database operations with an HTTP server.
*/
interface BaseApi {
interface BaseDatabaseApi {
/**
* Handle the HEAD request
* This is used to check if a data exists in the database
*/
fun head(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
/**
* Handle the GET request
* This is used to get data from the database
* This is used to get all the data of a table from the database
*/
fun get(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
fun get(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
/**
* Handle the POST request
* This is used to insert data into the database
*/
fun post(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
fun post(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
/**
* Handle the PUT request
* This is used to delete data from the database
*/
fun delete(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
fun delete(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response
}

View file

@ -0,0 +1,95 @@
package com.faraphel.tasks_valider.database.api.server.entities.base
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.utils.getBody
import com.faraphel.tasks_valider.utils.parser
import fi.iki.elonen.NanoHTTPD
abstract class BaseTaskDatabaseApi<Entity> (
private val dao: BaseTaskDao<Entity>,
private val session: SessionEntity,
private val entityType: Class<Entity>,
) : BaseDatabaseApi {
/**
* Handle an HTTP HEAD request.
* Indicate if an object exist in the database.
* @param httpSession the current http session to handle.
* @return a response indicating if the object does exist.
*/
override fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
// get the content of the request
val data = httpSession.getBody()
// parse the object
val obj = parser.fromJson(data, entityType)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(obj)
// return the response
return NanoHTTPD.newFixedLengthResponse(
if (exists) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND,
"text/plain",
if (exists) "Exists" else "Not found"
)
}
/**
* Handle an HTTP GET request.
* Indicate the content of a table in the database.
* @param httpSession the current http session to handle.
* @return a response indicating the content of all the objects.
*/
override fun get(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
// return the content of all the objects in the database about this session
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
/**
* Handle an HTTP POST request.
* Create a new object in the database.
* @param httpSession the current http session to handle.
* @return the id of the newly created object
*/
override fun post(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
// get the content of the request
val data = httpSession.getBody()
// parse the object
val obj = parser.fromJson(data, entityType)
// insert it into the database
val id = this.dao.insert(obj)
// return the id of the object created
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
/**
* Handle an HTTP DELETE request.
* Delete an item from the database.
* @param httpSession the current http session to handle.
* @return the number of object deleted.
*/
override fun delete(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
// get the content of the request
val data = httpSession.getBody()
// parse the object
val obj = parser.fromJson(data, entityType)
// delete all the instance of this object in the database
val count = this.dao.delete(obj)
// return the number of corresponding element deleted
return NanoHTTPD.newFixedLengthResponse(
if (count > 0) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND,
"text/plain",
count.toString()
)
}
}

View file

@ -1,20 +0,0 @@
package com.faraphel.tasks_valider.database.converters
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.room.TypeConverter
import java.time.Instant
class InstantConverter {
@RequiresApi(Build.VERSION_CODES.O)
@TypeConverter
fun fromTimestamp(value: Long?): Instant? {
return value?.let { Instant.ofEpochMilli(it) }
}
@RequiresApi(Build.VERSION_CODES.O)
@TypeConverter
fun dateToTimestamp(instant: Instant?): Long? {
return instant?.toEpochMilli()
}
}

View file

@ -2,22 +2,32 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
@Dao
interface ClassDao : BaseDao<ClassEntity> {
interface ClassDao : BaseTaskDao<ClassEntity> {
@Query("SELECT * FROM ${ClassEntity.TABLE_NAME}")
override fun getAll(): List<ClassEntity>
@Query(
"SELECT * FROM ${ClassEntity.TABLE_NAME} " +
"JOIN ${SessionEntity.TABLE_NAME} " +
"ON ${SessionEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " +
"WHERE ${SessionEntity.TABLE_NAME}.id = :sessionId"
)
override fun getAllBySession(sessionId: Long): List<ClassEntity>
/**
* Get the object from its identifier
*/
@Query("SELECT * FROM ${ClassEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): ClassEntity
fun getById(id: Long): ClassEntity?
/**
* Get all the sessions this class attended
@ -31,7 +41,7 @@ interface ClassDao : BaseDao<ClassEntity> {
* @param id the id of the class
*/
@Query(
"SELECT * FROM ${PersonEntity.TABLE_NAME} " +
"SELECT ${PersonEntity.TABLE_NAME}.* FROM ${PersonEntity.TABLE_NAME} " +
"JOIN ${RelationClassPersonEntity.TABLE_NAME} " +
"ON ${PersonEntity.TABLE_NAME}.id = ${RelationClassPersonEntity.TABLE_NAME}.student_id " +
"WHERE ${RelationClassPersonEntity.TABLE_NAME}.class_id = :id"

View file

@ -2,11 +2,13 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.*
@Dao
interface PersonDao : BaseDao<PersonEntity> {
interface PersonDao : BaseTaskDao<PersonEntity> {
@Query("SELECT * FROM ${PersonEntity.TABLE_NAME}")
override fun getAll(): List<PersonEntity>
@ -14,7 +16,20 @@ interface PersonDao : BaseDao<PersonEntity> {
* Get the object from its identifier
*/
@Query("SELECT * FROM ${PersonEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): PersonEntity
fun getById(id: Long): PersonEntity?
@Query(
"SELECT * FROM ${PersonEntity.TABLE_NAME} " +
"WHERE id IN (" +
"SELECT student_id FROM ${RelationClassPersonEntity.TABLE_NAME} " +
"JOIN ${ClassEntity.TABLE_NAME} " +
"ON ${RelationClassPersonEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " +
"JOIN ${SessionEntity.TABLE_NAME} " +
"ON ${SessionEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " +
"WHERE ${SessionEntity.TABLE_NAME}.id = :sessionId" +
")"
)
override fun getAllBySession(sessionId: Long): List<PersonEntity>
/**
* Allow to get all the classes the person is attending as a student

View file

@ -2,22 +2,35 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
@Dao
interface RelationClassPersonDao : BaseDao<RelationClassPersonEntity> {
interface RelationClassPersonDao : BaseTaskDao<RelationClassPersonEntity> {
@Query("SELECT * FROM ${RelationClassPersonEntity.TABLE_NAME}")
override fun getAll(): List<RelationClassPersonEntity>
@Query(
"SELECT ${RelationClassPersonEntity.TABLE_NAME}.* FROM ${RelationClassPersonEntity.TABLE_NAME} " +
"JOIN ${ClassEntity.TABLE_NAME} " +
"ON ${RelationClassPersonEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " +
"JOIN ${SessionEntity.TABLE_NAME} " +
"ON ${SessionEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " +
"WHERE ${SessionEntity.TABLE_NAME}.id = :sessionId"
)
override fun getAllBySession(sessionId: Long): List<RelationClassPersonEntity>
/**
* Get the object from its identifiers
*/
@Query(
"SELECT * FROM ${RelationClassPersonEntity.TABLE_NAME} " +
"WHERE " +
"class_id = :classId AND " +
"student_id = :studentId"
"WHERE class_id = :classId " +
"AND student_id = :studentId"
)
fun getById(classId: Long, studentId: Long): RelationClassPersonEntity
fun getById(classId: Long, studentId: Long): RelationClassPersonEntity?
}

View file

@ -0,0 +1,32 @@
package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
@Dao
interface RelationPersonSessionSubjectDao : BaseTaskDao<RelationPersonSessionSubjectEntity> {
@Query("SELECT * FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME}")
override fun getAll(): List<RelationPersonSessionSubjectEntity>
@Query(
"SELECT * FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " +
"WHERE session_id = :sessionId"
)
override fun getAllBySession(sessionId: Long): List<RelationPersonSessionSubjectEntity>
/**
* Get the object from its identifiers
*/
@Query(
"SELECT * FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " +
"WHERE student_id = :studentId " +
"AND session_id = :sessionId " +
"AND subject_id = :subjectId"
)
fun getById(studentId: Long, sessionId: Long, subjectId: Long): RelationPersonSessionSubjectEntity?
}

View file

@ -2,17 +2,23 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
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
@Dao
interface SessionDao : BaseDao<SessionEntity> {
interface SessionDao : BaseTaskDao<SessionEntity> {
@Query("SELECT * FROM ${SessionEntity.TABLE_NAME}")
override fun getAll(): List<SessionEntity>
@Query("SELECT * FROM ${SessionEntity.TABLE_NAME} WHERE id = :sessionId")
override fun getAllBySession(sessionId: Long): List<SessionEntity>
/**
* Get the object from its identifier
*/
@Query("SELECT * FROM ${SessionEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): SessionEntity
fun getById(id: Long): SessionEntity?
}

View file

@ -2,26 +2,38 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
import com.faraphel.tasks_valider.database.entities.SubjectEntity
import com.faraphel.tasks_valider.database.entities.TaskEntity
@Dao
interface SubjectDao : BaseDao<SubjectEntity> {
interface SubjectDao : BaseTaskDao<SubjectEntity> {
@Query("SELECT * FROM ${SubjectEntity.TABLE_NAME}")
override fun getAll(): List<SubjectEntity>
@Query(
"SELECT * FROM ${SubjectEntity.TABLE_NAME} " +
"WHERE id IN (" +
"SELECT subject_id FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " +
"WHERE session_id = :sessionId" +
")"
)
override fun getAllBySession(sessionId: Long): List<SubjectEntity>
/**
* Get the object from its identifier
*/
@Query("SELECT * FROM ${SubjectEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): SubjectEntity
fun getById(id: Long): SubjectEntity?
/**
* Get all the tasks available in a subject
* @param id the id of the subject
*/
@Query("SELECT * FROM ${TaskEntity.TABLE_NAME} WHERE subject_id = :id")
fun getSessions(id: Long): List<SessionEntity>
fun getTasks(id: Long): List<TaskEntity>
}

View file

@ -2,20 +2,32 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
import com.faraphel.tasks_valider.database.entities.TaskEntity
import com.faraphel.tasks_valider.database.entities.ValidationEntity
@Dao
interface TaskDao : BaseDao<TaskEntity> {
interface TaskDao : BaseTaskDao<TaskEntity> {
@Query("SELECT * FROM ${TaskEntity.TABLE_NAME}")
override fun getAll(): List<TaskEntity>
@Query(
"SELECT * FROM ${TaskEntity.TABLE_NAME} " +
"WHERE subject_id IN (" +
"SELECT subject_id FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " +
"WHERE session_id = :sessionId" +
")"
)
override fun getAllBySession(sessionId: Long): List<TaskEntity>
/**
* Get the object from its identifier
*/
@Query("SELECT * FROM ${TaskEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): TaskEntity
fun getById(id: Long): TaskEntity?
/**
* Get all the validations have been approved for this tasks

View file

@ -2,14 +2,29 @@ package com.faraphel.tasks_valider.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
import com.faraphel.tasks_valider.database.entities.TaskEntity
import com.faraphel.tasks_valider.database.entities.ValidationEntity
@Dao
interface ValidationDao : BaseDao<ValidationEntity> {
interface ValidationDao : BaseTaskDao<ValidationEntity> {
@Query("SELECT * FROM ${ValidationEntity.TABLE_NAME}")
override fun getAll(): List<ValidationEntity>
@Query(
"SELECT * FROM ${ValidationEntity.TABLE_NAME} " +
"JOIN ${TaskEntity.TABLE_NAME} " +
"ON ${ValidationEntity.TABLE_NAME}.task_id = ${TaskEntity.TABLE_NAME}.id " +
"WHERE ${TaskEntity.TABLE_NAME}.subject_id IN (" +
"SELECT subject_id FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " +
"WHERE session_id = :sessionId" +
")"
)
override fun getAllBySession(sessionId: Long): List<ValidationEntity>
/**
* Get the object from its identifiers
*/
@ -20,5 +35,5 @@ interface ValidationDao : BaseDao<ValidationEntity> {
"student_id = :studentId and " +
"task_id = :taskId"
)
fun getById(teacherId: Long, studentId: Long, taskId: Long): ValidationEntity
fun getById(teacherId: Long, studentId: Long, taskId: Long): ValidationEntity?
}

View file

@ -3,17 +3,20 @@ package com.faraphel.tasks_valider.database.dao.base
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Update
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
/**
* A base DAO to handle the database operations.
* A base DAO to handle the database operations (CRON).
* @param Entity the entity to handle
*/
interface BaseDao<Entity> {
interface BaseCronDao<Entity> {
/**
* Check if the entity exists in the database.
*/
fun exists(entity: Entity): Boolean {
return this.getAll().contains(entity)
}
/**
* Check if the entities exists in the database.
*/
@ -21,12 +24,24 @@ interface BaseDao<Entity> {
return this.getAll().containsAll(entities.toList())
}
/**
* Insert the entities into the database.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(entity: Entity): Long
/**
* Insert the entities into the database.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg entities: Entity): List<Long>
/**
* Delete the entity from the database.
*/
@Delete
fun delete(entity: Entity): Int
/**
* Delete the entities from the database.
*/

View file

@ -0,0 +1,11 @@
package com.faraphel.tasks_valider.database.dao.base
/**
* The base for a DAO that is inside a session scope.
*/
interface BaseSessionDao<Entity> {
/**
* Return all the objects concerned by a session.
*/
fun getAllBySession(sessionId: Long): List<Entity>
}

View file

@ -0,0 +1,6 @@
package com.faraphel.tasks_valider.database.dao.base
/**
* The base for a DAO for the task project.
*/
interface BaseTaskDao<Entity>: BaseCronDao<Entity>, BaseSessionDao<Entity>

View file

@ -3,17 +3,59 @@ package com.faraphel.tasks_valider.database.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import kotlinx.serialization.Serializable
import java.security.MessageDigest
import java.util.*
@Serializable
@Entity(tableName = PersonEntity.TABLE_NAME)
data class PersonEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("first_name") val firstName: String,
@ColumnInfo("last_name") val lastName: String,
@ColumnInfo("card_id") val cardId: UUID,
@ColumnInfo("card_id") val cardId: String? = null,
@ColumnInfo("password_hash") val passwordHash: String? = null,
@ColumnInfo("role") val role: TaskRole = TaskRole.STUDENT,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "persons"
/**
* Hash a password
*/
fun hashPassword(password: String): String {
val digester = MessageDigest.getInstance("SHA-256")
val hash = digester.digest(password.toByteArray())
return hash.joinToString("") { byte -> "%02x".format(byte) }
}
}
/**
* Constructor with string password
*/
constructor(
firstName: String,
lastName: String,
cardId: String? = null,
password: String? = null,
role: TaskRole,
) : this(
firstName = firstName,
lastName = lastName,
cardId = cardId,
passwordHash = password?.let { hashPassword(password) },
role = role,
)
/**
* Return the full name of the person
*/
fun fullName(): String = "${this.firstName.capitalize(Locale.ROOT)} ${this.lastName.uppercase()}"
/**
* Check if the password is correct
*/
fun checkPassword(password: String): Boolean = this.passwordHash == hashPassword(password)
}

View file

@ -0,0 +1,48 @@
package com.faraphel.tasks_valider.database.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
/**
* Represent the relation that associate a subject to a person for a specific session.
*/
@Entity(
tableName = RelationPersonSessionSubjectEntity.TABLE_NAME,
primaryKeys = [
"student_id",
"session_id",
"subject_id"
],
foreignKeys = [
ForeignKey(
entity = PersonEntity::class,
parentColumns = ["id"],
childColumns = ["student_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = SessionEntity::class,
parentColumns = ["id"],
childColumns = ["session_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = SubjectEntity::class,
parentColumns = ["id"],
childColumns = ["subject_id"],
onDelete = ForeignKey.CASCADE
),
]
)
data class RelationPersonSessionSubjectEntity (
@ColumnInfo("student_id", index = true) val studentId: Long,
@ColumnInfo("session_id", index = true) val sessionId: Long,
@ColumnInfo("subject_id", index = true) val subjectId: Long,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "relation_person_session_subject"
}
}

View file

@ -21,6 +21,7 @@ data class TaskEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("title") val title: String,
@ColumnInfo("description") val description: String? = null,
@ColumnInfo("order") val order: Int, ///< the order of the task in the list of the subject
@ColumnInfo("subject_id", index = true) val subjectId: Long,
) : BaseEntity() {

View file

@ -35,14 +35,28 @@ import java.time.Instant
]
)
data class ValidationEntity (
@ColumnInfo("date") val date: Instant,
@ColumnInfo("teacher_id", index = true) val teacherId: Long,
@ColumnInfo("student_id", index = true) val studentId: Long,
@ColumnInfo("task_id", index = true) val taskId: Long,
@ColumnInfo("date") val date: Instant,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "validations"
}
}
/**
* Construct a new ValidationEntity. Automatically set the date to today.
*/
constructor(
teacherId: Long,
studentId: Long,
taskId: Long
):
this(
teacherId = teacherId,
studentId = studentId,
taskId = taskId,
date = Instant.now()
)
}

View file

@ -1,3 +1,7 @@
package com.faraphel.tasks_valider.database.entities.base
open class BaseEntity
open class BaseEntity {
companion object {
const val TABLE_NAME = "<Undefined>"
}
}

View file

@ -0,0 +1,6 @@
package com.faraphel.tasks_valider.database.entities.error
class HttpException(
private val code: Int,
) : Exception("Http Exception: $code")

View file

@ -0,0 +1,133 @@
package com.faraphel.tasks_valider.database
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.database.entities.*
fun populateTaskDatabaseTest(database: TaskDatabase) {
// classes
val (
classMiageId,
classIsriId,
) = database.classDao().insert(
ClassEntity(name="MIAGE"),
ClassEntity(name="ISRI")
)
val classMiage = database.classDao().getById(classMiageId)!!
val classIrsi = database.classDao().getById(classIsriId)!!
// persons
val (
personBillyId,
personBobbyId,
personBettyId,
) = database.personDao().insert(
PersonEntity(
"Billy", "Bob",
"0A1A7553-9DE5-103C-B23C-630998207116",
"1234",
TaskRole.STUDENT
),
PersonEntity(
"Bobby", "Bob",
null,
"1234",
TaskRole.STUDENT
),
PersonEntity(
"Betty", "Bob",
null,
"1234",
TaskRole.STUDENT
)
)
val personBilly = database.personDao().getById(personBillyId)!!
val personBobby = database.personDao().getById(personBobbyId)!!
val personBetty = database.personDao().getById(personBettyId)!!
// relations class <=> persons
database.relationClassPersonDao().insert(
RelationClassPersonEntity(personBobby.id, classMiage.id),
RelationClassPersonEntity(personBilly.id, classMiage.id),
RelationClassPersonEntity(personBetty.id, classIrsi.id),
)
// subjects
val (
subjectAId,
subjectBId
) = database.subjectDao().insert(
SubjectEntity(
name="Type A"
),
SubjectEntity(
name="Type B"
)
)
val subjectA = database.subjectDao().getById(subjectAId)!!
val subjectB = database.subjectDao().getById(subjectBId)!!
// tasks
val (
taskA1Id,
taskA2Id,
taskA3Id,
taskB1Id,
taskB2Id,
) = database.taskDao().insert(
TaskEntity(
title = "Commencer A",
description = "Description 1",
order = 1,
subjectId = subjectA.id,
),
TaskEntity(
title = "Continuer A",
description = "Description 2",
order = 2,
subjectId = subjectA.id
),
TaskEntity(
title = "Finir A",
description = "Description 3",
order = 3,
subjectId = subjectA.id
),
TaskEntity(
title = "Commencer B",
description = "Description 1",
order = 1,
subjectId = subjectB.id,
),
TaskEntity(
title = "Finir B",
description = "Description 2",
order = 2,
subjectId = subjectB.id,
)
)
val taskA1 = database.taskDao().getById(taskA1Id)!!
val taskA2 = database.taskDao().getById(taskA2Id)!!
val taskA3 = database.taskDao().getById(taskA3Id)!!
val taskB1 = database.taskDao().getById(taskB1Id)!!
val taskB2 = database.taskDao().getById(taskB2Id)!!
}
fun populateSubjectSessionPersonTest(database: TaskDatabase, session: SessionEntity) {
// get the list of available subjects
val subjects = database.subjectDao().getAll()
// give a random subject to everyone in the session
database.personDao().getAllBySession(session.id).forEach { person ->
// get a random subject
val subject = subjects.random()
// insert a new subject for a person for a specific session
database.relationPersonSessionSubjectDao().insert(
RelationPersonSessionSubjectEntity(person.id, session.id, subject.id)
)
}
}

View file

@ -0,0 +1,40 @@
package com.faraphel.tasks_valider.ui.screen.authentification
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.PasswordVisualTransformation
/**
* Authentification screen where the client can give his information
*/
@Composable
fun AuthentificationClientScreen() {
val cardId = remember { mutableStateOf("") }
val password = remember { mutableStateOf("") }
Column {
// card identifier
TextField(
value = cardId.value,
onValueChange = { text -> cardId.value = text },
)
// password
TextField(
value = password.value,
visualTransformation = PasswordVisualTransformation(),
onValueChange = { text -> password.value = text },
)
// button
Button(onClick = { /* do something */ }) {
Text("Submit")
}
}
}

View file

@ -0,0 +1,70 @@
package com.faraphel.tasks_valider.ui.screen.authentification
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.database.entities.PersonEntity
/**
* Authentification screen where the host can give his information
*/
@Composable
fun AuthentificationServerScreen(personEntity: MutableState<PersonEntity?>) {
val firstName = remember { mutableStateOf("") }
val lastName = remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Your Profile",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// first name
TextField(
value = firstName.value,
placeholder = { Text("first name") },
onValueChange = { text -> firstName.value = text },
)
// last name
TextField(
value = lastName.value,
placeholder = { Text("last name") },
onValueChange = { text -> lastName.value = text },
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// confirm button
Button(onClick = {
// create the person entity with the given information
personEntity.value = PersonEntity(
firstName = firstName.value,
lastName = lastName.value,
role = TaskRole.ADMIN
)
}) {
Text("Submit")
}
}
}

View file

@ -1,6 +1,8 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet.client
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
@ -16,15 +18,16 @@ import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_ADDRESS
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
@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

@ -1,33 +1,52 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
import android.app.Activity
import androidx.compose.foundation.layout.Column
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.ui.screen.communication.internet.client.CommunicationInternetClientScreen
import com.faraphel.tasks_valider.ui.screen.communication.internet.server.CommunicationInternetServerScreen
import com.faraphel.tasks_valider.database.TaskDatabase
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetScreen(activity: Activity) {
fun CommunicationInternetSelectScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") {
composable("mode") { CommunicationInternetSelectContent(controller) }
composable("client") { CommunicationInternetClientScreen(activity) }
composable("server") { CommunicationInternetServerScreen(activity) }
composable("server") { CommunicationInternetServerScreen(activity, database) }
}
}
@Composable
fun CommunicationInternetSelectContent(controller: NavController) {
Column {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Role",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// client mode
Button(onClick = { controller.navigate("client") }) { Text("Client") }
// server mode

View file

@ -0,0 +1,218 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
import android.app.Activity
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.TaskServer
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.populateSubjectSessionPersonTest
import com.faraphel.tasks_valider.ui.screen.authentification.AuthentificationServerScreen
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.TaskSessionController
import java.time.Instant
/**
* Screen for the host to configure the server
*/
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetServerScreen(
activity: Activity,
database: TaskDatabase,
) {
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 (adminPersonEntityRaw.value == null) AuthentificationServerScreen(adminPersonEntityRaw)
else controller.navigate("configuration")
}
composable("configuration") {
if (client.value == null)
CommunicationInternetServerContent(
database,
adminPersonEntityRaw,
adminPersonEntity,
client,
)
else controller.navigate("session")
}
composable("session") {
TaskSessionController(
activity,
client.value!!,
adminPersonEntity.value!!
)
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetServerContent(
database: TaskDatabase,
adminPersonEntityRaw: MutableState<PersonEntity?>,
adminPersonEntity: MutableState<PersonEntity?>,
client: MutableState<TaskClient?>
) {
val classes = remember { mutableStateOf<List<ClassEntity>?>(null) }
val selectedClass = remember { mutableStateOf<ClassEntity?>(null) }
val areClassesExpanded = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
LaunchedEffect(true) {
// refresh the list of classes
Thread { refreshClasses(database, classes) }.start()
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "New Session",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// classes
Row(verticalAlignment = Alignment.CenterVertically) {
// description
Text(text = "Class", fontSize = 12.sp)
// separator
Spacer(modifier = Modifier.width(width = 12.dp))
// selector
Button(onClick = { areClassesExpanded.value = !areClassesExpanded.value }) {
// display the selected class, if selected
if (selectedClass.value != null) Text(text = selectedClass.value!!.name)
else Text(text = "<Not selected>")
}
// class selector
DropdownMenu(
expanded = areClassesExpanded.value,
onDismissRequest = { areClassesExpanded.value = false }
) {
// TODO(Faraphel): student lists should be loaded from the database or a file
classes.value?.forEach { class_ ->
DropdownMenuItem(
text = { Text(class_.name) },
onClick = {
selectedClass.value = class_
areClassesExpanded.value = false
}
)
}
}
}
// server port
Row(verticalAlignment = Alignment.CenterVertically) {
// descriptor
Text(text = "Port")
// separator
Spacer(modifier = Modifier.width(width = 12.dp))
// input
TextField(
modifier = Modifier.width(80.dp),
value = serverPort.intValue.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
onValueChange = { text ->
val port = text.toInt()
if (port in RANGE_SERVER_PORT) {
serverPort.intValue = port
}
}
)
}
// check if a class is selected
if (selectedClass.value != null)
// button to create the server
Button(onClick = {
Thread { // a thread is used for networking
// 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,
)
)
val session = database.sessionDao().getById(sessionId)!!
// TODO(Faraphel): remove, this is a test function
Thread {
populateSubjectSessionPersonTest(database, session)
}.let { thread ->
thread.start()
thread.join()
}
// Create the server
Log.i("room-server", "creating the server")
val server = TaskServer(
serverPort.intValue,
database,
session,
adminPersonEntity.value!!,
)
server.start()
// Get the client from the server
client.value = server.getAdminClient()
}.start()
}) {
Text("Create")
}
}
}
/**
* Refresh the list of classes
*/
fun refreshClasses(database: TaskDatabase, classes: MutableState<List<ClassEntity>?>) {
classes.value = database.classDao().getAll()
}

View file

@ -1,99 +0,0 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet.server
import android.app.Activity
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import androidx.room.Room
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.TaskServer
import com.faraphel.tasks_valider.database.TaskDatabase
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
@Composable
fun CommunicationInternetServerScreen(activity: Activity) {
val client = remember { mutableStateOf<TaskClient?>(null) }
// if the server is not created, prompt the user for the server configuration
if (client.value == null) CommunicationInternetServerContent(activity, client)
// else, go to the base tasks screen
else TaskSessionScreen(activity, client.value!!)
}
@Composable
fun CommunicationInternetServerContent(activity: Activity, client: MutableState<TaskClient?>) {
val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
Column {
// student list
Button(onClick = { expandedStudentList.value = !expandedStudentList.value }) {
Text(text = "Select Students List")
}
DropdownMenu(
expanded = expandedStudentList.value,
onDismissRequest = { expandedStudentList.value = false }
) {
DropdownMenuItem(
text = { Text("ISRI") },
onClick = {}
)
DropdownMenuItem(
text = { Text("MIAGE") },
onClick = {}
)
// TODO(Faraphel): student lists should be loaded from the database or a file
}
// server port
TextField(
value = serverPort.intValue.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { text ->
val port = text.toInt()
if (port in RANGE_SERVER_PORT) {
serverPort.intValue = port
}
}
)
Button(onClick = {
// Reset the database | TODO(Faraphel): only for testing purpose
activity.deleteDatabase("local")
// Create the database
val database = Room.databaseBuilder(
activity,
TaskDatabase::class.java,
"local"
).build()
// Create the server
Log.i("room-server", "creating the server")
Thread { // a thread is used for networking
val server = TaskServer(serverPort.intValue, database)
server.start()
// Get the client from the server
client.value = server.getClientAdmin()
}.start()
}) {
Text("Create")
}
}
}

View file

@ -1,19 +1,27 @@
package com.faraphel.tasks_valider.ui.screen.communication
import android.app.Activity
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.ui.screen.communication.internet.CommunicationInternetScreen
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.internet.CommunicationInternetSelectScreen
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.CommunicationWifiP2pScreen
@ -21,10 +29,13 @@ import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.CommunicationW
* CommunicationController is the main controller for the communication screen.
* It is responsible for handling the navigation between the different communication methods.
* It is also responsible for initializing the communication methods.
*
* @param activity: The activity that hosts the communication screen.
* @param database: the database.
*/
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationScreen(activity: Activity) {
fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController()
NavHost(
@ -35,11 +46,11 @@ fun CommunicationScreen(activity: Activity) {
CommunicationSelectContent(controller, activity)
}
composable("internet") {
CommunicationInternetScreen(activity)
CommunicationInternetSelectScreen(activity, database)
}
composable("wifi-p2p") {
val bwfManager = BwfManager.fromActivity(activity)
CommunicationWifiP2pScreen(activity, bwfManager)
val bwdManager = BwdManager.fromActivity(activity)
CommunicationWifiP2pScreen(activity, bwdManager)
}
}
}
@ -50,9 +61,22 @@ fun CommunicationScreen(activity: Activity) {
*/
@Composable
fun CommunicationSelectContent(controller: NavController, activity: Activity) {
val isWifiP2pSupported = BwfManager.isSupported(activity)
val isWifiP2pSupported = BwdManager.isSupported(activity)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Connection Type",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
Column {
// internet communication mode
Button(onClick = { controller.navigate("internet") }) {
Text("Internet")
@ -62,7 +86,7 @@ fun CommunicationSelectContent(controller: NavController, activity: Activity) {
Button(
colors = ButtonDefaults.buttonColors(
// if the WiFi-Direct is not supported, the button is grayed out
containerColor = if (isWifiP2pSupported) Color.Unspecified else Color.Gray
containerColor = if (isWifiP2pSupported) MaterialTheme.colorScheme.primary else Color.Gray
),
onClick = {
// if the WiFi-Direct is supported, navigate to the WiFi-Direct screen

View file

@ -8,12 +8,12 @@ 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.bwf.BwfManager
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget
@Composable
fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager) {
fun CommunicationWifiP2pClientScreen(activity: Activity, bwdManager: BwdManager) {
val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) }
val isConnected = remember { mutableStateOf(false) }
@ -31,25 +31,25 @@ fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager)
val config = WifiP2pConfig().apply {
deviceAddress = selectedDevice.value!!.deviceAddress
}
bwfManager.connect(config) {
bwdManager.connect(config) {
isConnected.value = true
}
return
}
// display the list of devices
CommunicationWifiP2pClientContent(bwfManager, selectedDevice)
CommunicationWifiP2pClientContent(bwdManager, selectedDevice)
}
@Composable
fun CommunicationWifiP2pClientContent(
bwfManager: BwfManager,
bwdManager: BwdManager,
selectedDevice: MutableState<WifiP2pDevice?>
) {
Column {
WifiP2pDeviceListWidget(
peers = bwfManager.statePeers.value,
peers = bwdManager.statePeers.value,
filter = { device: WifiP2pDevice -> device.isGroupOwner },
selectedDevice,
)

View file

@ -9,19 +9,19 @@ import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.client.CommunicationWifiP2pClientScreen
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server.CommunicationWifiP2pServerScreen
@Composable
fun CommunicationWifiP2pScreen(activity: Activity, bwfManager: BwfManager) {
fun CommunicationWifiP2pScreen(activity: Activity, bwdManager: BwdManager) {
val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") {
composable("mode") { CommunicationWifiP2pSelectContent(controller) }
composable("client") { CommunicationWifiP2pClientScreen(activity, bwfManager) }
composable("server") { CommunicationWifiP2pServerScreen(activity, bwfManager) }
composable("client") { CommunicationWifiP2pClientScreen(activity, bwdManager) }
composable("server") { CommunicationWifiP2pServerScreen(activity, bwdManager) }
}
}

View file

@ -1,47 +1,33 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server
import android.app.Activity
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import androidx.room.Room
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.TaskServer
import com.faraphel.tasks_valider.database.TaskDatabase
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
@Composable
fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager) {
fun CommunicationWifiP2pServerScreen(activity: Activity, bwdManager: BwdManager) {
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!!)
}
@Composable
fun CommunicationWifiP2pServerContent(
activity: Activity,
bwfManager: BwfManager,
bwdManager: BwdManager,
client: MutableState<TaskClient?>
) {
/*
val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -90,17 +76,30 @@ fun CommunicationWifiP2pServerContent(
"local"
).build()
// Create the admin
// TODO: the admin should be created from the card reader
val adminPersonEntity = PersonEntity(
"admin",
"admin",
"123456789",
"admin",
)
// Insert the admin in the database
database.personDao().insert(adminPersonEntity)
bwfManager.recreateGroup {
// Create the server
Log.i("room-server", "creating the server")
val server = TaskServer(serverPort.intValue, database)
val server = TaskServer(serverPort.intValue, database, adminPersonEntity)
server.start()
// Get the client from the server
client.value = server.getClientAdmin()
client.value = server.getAdminClient()
}
}) {
Text("Create")
}
}
*/
}

View file

@ -0,0 +1,37 @@
package com.faraphel.tasks_valider.ui.screen.scan.qr
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.BarcodeResult
import com.journeyapps.barcodescanner.DecoratedBarcodeView
import com.journeyapps.barcodescanner.DefaultDecoderFactory
/**
* Screen to scan a Barcode / QR Code
*/
@Composable
fun ScanBarcodeScreen(activity: Activity, barcode: MutableState<BarcodeResult?>) {
Box(modifier = Modifier.fillMaxSize()) {
// check and prompt for the camera permission
if (activity.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)
// FIXME(Faraphel): seem to crash the application
activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), 1)
// AndroidView is used because "DecoratedBarcodeView" only support the legacy view system
AndroidView(factory = {
DecoratedBarcodeView(activity).apply {
this.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
this.initializeFromIntent(activity.intent)
this.decodeContinuous { result -> barcode.value = result }
this.resume()
}
})
}
}

View file

@ -0,0 +1,128 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.navigation.NavController
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.ValidationEntity
import com.faraphel.tasks_valider.ui.screen.scan.qr.ScanBarcodeScreen
import com.journeyapps.barcodescanner.BarcodeResult
import okhttp3.HttpUrl.Companion.toHttpUrl
@Composable
fun QuickValidationScreen(
controller: NavController,
activity: Activity,
client: TaskClient,
user: PersonEntity,
) {
val barcode = remember { mutableStateOf<BarcodeResult?>(null) }
// prompt for the qr code if not found
if (barcode.value == null)
return ScanBarcodeScreen(activity, barcode)
// show the content of the qr code
val studentUrl = barcode.value!!.text.toHttpUrl()
val cardId = studentUrl.pathSegments[0]
// when the barcode changed
LaunchedEffect(cardId) {
Thread {
quickValidation(
controller,
activity,
client,
user,
cardId,
)
}.start()
}
}
/**
* Validate the latest task of a user from its student card
*/
fun quickValidation(
controller: NavController,
activity: Activity,
client: TaskClient,
user: PersonEntity,
cardId: String,
) {
// action when an error occurred or everything worked
fun finish() {
activity.runOnUiThread {
// go back to the main screen
controller.navigateUp()
}
}
// requests all the persons
val allPersons = client.personApi.getAll()
// get the person with the matching card
val person = allPersons.firstOrNull { person -> person.cardId == cardId }
if (person == null) {
// tell to the user that this card is linked to nobody
activity.runOnUiThread {
Toast.makeText(activity, "No person found for that card.", Toast.LENGTH_LONG).show()
}
return finish()
}
// requests all the relation persons - subjects
val allRelationsPersonSubject = client.relationPersonSessionSubjectApi.getAll()
// get the corresponding relation
val relationPersonSubject = allRelationsPersonSubject.first { relation -> relation.studentId == person.id }
// requests all the tasks
val allTasks = client.taskApi.getAll()
// get the corresponding tasks
val tasks = allTasks
.filter { task -> task.subjectId == relationPersonSubject.subjectId }
.sortedBy { task -> task.order }
// requests all the validations
val allValidations = client.validationApi.getAll()
// get the corresponding relation
val validations = allValidations.filter { validation -> validation.studentId == person.id }
// get the first task without any validation
val task = tasks.firstOrNull { task ->
// check in all the validations if the task is found
val validation = validations.firstOrNull { validation -> validation.taskId == task.id }
// keep the task if it has no validation
validation == null
}
if (task == null) {
// tell to the user the action cannot be done
activity.runOnUiThread {
Toast.makeText(activity, "There are no tasks left.", Toast.LENGTH_LONG).show()
}
return finish()
}
// create a new validation on the server
client.validationApi.save(
ValidationEntity(
teacherId=user.id,
studentId=person.id,
taskId=task.id,
)
)
// confirm to the user the action was successful
activity.runOnUiThread {
Toast.makeText(activity, "Validated \"${task.title}\".", Toast.LENGTH_LONG).show()
}
return finish()
}

View file

@ -1,55 +0,0 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.faraphel.tasks_valider.connectivity.task.TaskClient
/**
* This screen represent a session
* @param activity the android activity
* @param client an HTTP client that can communicate with the server
*/
@Composable
fun TaskSessionScreen(activity: Activity, client: TaskClient) {
Text("WIP : Session Screen")
/*
val students = remember { mutableStateOf<List<TaskGroupEntity>?>(null) }
// title
Text(text = "Task Group")
// if the groups are not yet defined, refresh the list
if (groups.value == null) {
Thread { refreshGroups(activity, client, groups) }.start()
return
}
// if the groups have already been defined, display them
for (group in groups.value!!) {
Text(text = group.toString())
}
*/
}
/*
fun refreshGroups(activity: Activity, client: TaskClient, groups: MutableState<List<TaskGroupEntity>?>) {
// try to obtain the list of groups
val response = client.get("entities/group")
// in case of error, notify it
if (!response.isSuccessful) {
Toast.makeText(activity, response.message, Toast.LENGTH_LONG).show()
return
}
// parse the list of groups
groups.value = jsonParser.fromJson(
response.body.toString(),
object : TypeToken<List<TaskGroupEntity>>(){}
)
}
*/

View file

@ -0,0 +1,189 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.utils.parser
import java.io.File
@Composable
fun TaskSessionController(
activity: Activity,
client: TaskClient,
user: PersonEntity,
) {
val controller = rememberNavController()
NavHost(
navController = controller,
startDestination = "main"
) {
composable("main") {
TaskSessionScreen(controller, activity, client, user)
}
composable("quick_validation") {
QuickValidationScreen(controller, activity, client, user)
}
}
}
/**
* This screen represent a session
* @param activity the android activity
* @param client an HTTP client that can communicate with the server
*/
@Composable
fun TaskSessionScreen(
controller: NavController,
activity: Activity,
client: TaskClient,
user: PersonEntity,
) {
val students = remember { mutableStateOf<List<PersonEntity>?>(null) }
val selectedStudent = remember { mutableStateOf<PersonEntity?>(null) }
// if the groups are not yet defined, refresh the list
if (students.value == null)
return LaunchedEffect(true) {
Thread { refreshStudents(activity, client, students) }.start()
}
if (selectedStudent.value != null)
return TaskStudentScreen(
activity,
client,
user,
selectedStudent.value!!
)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Session",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// if the groups have already been defined, display them
for (student in students.value!!) {
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp, horizontal = 16.dp),
onClick = { selectedStudent.value = student },
) {
Text(text = student.fullName())
}
}
// separator
Spacer(modifier = Modifier.weight(1f))
// buttons
Row {
// quick validation
Button(onClick = { controller.navigate("quick_validation") }) {
Text("Quick Validation")
}
Button(onClick = { Thread { exportToFile(activity, client) }.start() }) {
Text("Export")
}
}
}
}
fun refreshStudents(
activity: Activity,
client: TaskClient,
students: MutableState<List<PersonEntity>?>
) {
try {
// try to get all the persons in that session
students.value = client.personApi.getAll()
} catch (exception: Exception) {
// in case of error, show a message
return activity.runOnUiThread {
Log.e("students", "$exception")
Toast.makeText(activity, "Could not retrieve students.\n\n$exception", Toast.LENGTH_LONG).show()
}
}
}
fun exportToFile(
activity: Activity,
client: TaskClient,
) {
// get all the values to export
val allPersons = client.personApi.getAll()
val allStudents = allPersons.filter { student -> student.role == TaskRole.STUDENT }
val allRelationsStudentSessionSubject = client.relationPersonSessionSubjectApi.getAll()
val allSubjects = client.subjectApi.getAll()
val allTasks = client.taskApi.getAll()
val allValidations = client.validationApi.getAll()
// for each student
val data = allStudents.map { student ->
// fetch their subject
val relationStudentSessionObject = allRelationsStudentSessionSubject.first { relation ->
relation.studentId == student.id
}
val subject = allSubjects.first { subject -> subject.id == relationStudentSessionObject.subjectId }
// get the tasks of this person
val tasks = allTasks.filter { task -> task.subjectId == subject.id }
// for all the tasks
val studentTasksData = tasks.map { task ->
// if a validation match
val isValidated = allValidations.any { validation -> validation.taskId == task.id }
// return the pair of the task title and its validation
mapOf(
"name" to task.title,
"validated" to isValidated,
)
}
// return the name of the student associated to his data
mapOf(
"name" to student.fullName(),
"subject" to subject.name,
"tasks" to studentTasksData,
)
}
val dataJson = parser.toJson(data)
Log.i("export", dataJson)
// write all the data in a json file
val file = File(activity.filesDir, "test.json")
file.writeText(dataJson)
// TODO(Faraphel): prompt to open the file ?
}

View file

@ -0,0 +1,163 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.widget.Toast
import androidx.compose.foundation.layout.*
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 androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.faraphel.tasks_valider.utils.dateTimeFormatter
import com.faraphel.tasks_valider.utils.parser
import com.google.gson.reflect.TypeToken
/**
* This screen represent a student
* @param student the student object
*/
@Composable
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 || validations.value == null)
Thread {
refreshTasksValidations(
activity,
client,
student,
tasks,
validations
)
}.start()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(text = "Student", fontSize = 32.sp)
// student name - subtitle
Text(text = student.fullName(), fontSize = 24.sp)
// separator
Spacer(modifier = Modifier.height(24.dp))
// 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 }
Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 64.dp),
) {
Row {
Column {
// task title
Text(text = task.title, fontWeight = FontWeight.Bold)
// task description
task.description?.let { description -> Text(description) }
// if the task have been validated, show the date
if (validation != null) Text(text = dateTimeFormatter.format(validation.date))
}
// separator
Spacer(modifier = Modifier.fillMaxWidth())
// the validation state
Checkbox(
checked = validation != null,
enabled = user.role.permissions.contains(TaskPermission.WRITE),
onCheckedChange = { state ->
Thread {
// TODO(Faraphel): simplify or put the UI refresh in the update function ?
// send a notification to the server about the validation
updateValidation(
client,
state,
validation ?: ValidationEntity(
teacherId=user.id,
studentId=student.id,
taskId=task.id,
)
)
// refresh the UI
refreshTasksValidations(
activity,
client,
student,
tasks,
validations
)
}.start()
}
)
}
}
}
}
}
}
fun refreshTasksValidations(
activity: Activity,
client: TaskClient,
student: PersonEntity,
tasks: MutableState<List<TaskEntity>?>,
validations: MutableState<List<ValidationEntity>?>,
) {
// try to obtain the list of subject
val allRelationsPersonSessionSubject = client.relationPersonSessionSubjectApi.getAll()
// get the subject that the student is using
val relationPersonSessionSubject = allRelationsPersonSessionSubject.firstOrNull { relation ->
relation.studentId == student.id
}
if (relationPersonSessionSubject == null)
// TODO(Faraphel): should be able to assign a subject ?
return activity.runOnUiThread { Toast.makeText(activity, "No subject assigned", Toast.LENGTH_LONG).show() }
// try to obtain the list of tasks
val allTasks = client.taskApi.getAll()
// get the tasks that are linked to this subject
tasks.value = allTasks
.filter { task -> task.subjectId == relationPersonSessionSubject.subjectId }
.sortedBy { task -> task.order }
// try to obtain the list of validations
val allValidations = client.validationApi.getAll()
// 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) {
if (checked)
// if the validation is not set, create it
client.validationApi.save(validation)
else
// if the validation is set, delete it
client.validationApi.delete(validation)
}

View file

@ -0,0 +1,48 @@
package com.faraphel.tasks_valider.utils.converters
import androidx.room.TypeConverter
import com.google.gson.*
import java.lang.reflect.Type
import java.time.Instant
/**
* Allow for automatically converting Instant values when using them in the database or in a json parser.
*/
class InstantConverter : JsonDeserializer<Instant>, JsonSerializer<Instant> {
/**
* Convert a long into an instant
* @param value the number of milliseconds since the epoch of the time
* @return the Instant object
*/
@TypeConverter
fun deserialize(value: Long): Instant {
return Instant.ofEpochMilli(value)
}
/**
* Convert a long into an instant
* @param instant the Instant object
* @return the number of milliseconds since the epoch of the time
*/
@TypeConverter
fun serialize(instant: Instant): Long {
return instant.toEpochMilli()
}
/**
* Convert a long into an instant
* @param json the json object
* @return the Instant object
*/
override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): Instant =
this.deserialize(json.asLong)
/**
* Convert a long into an instant
* @param instant the instant object
* @return the json object
*/
override fun serialize(instant: Instant, type: Type, context: JsonSerializationContext): JsonElement =
JsonPrimitive(this.serialize(instant))
}

View file

@ -0,0 +1,11 @@
package com.faraphel.tasks_valider.utils
import com.faraphel.tasks_valider.utils.converters.InstantConverter
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.time.Instant
val parser: Gson = GsonBuilder()
.registerTypeAdapter(Instant::class.java, InstantConverter())
.create()

View file

@ -0,0 +1,22 @@
package com.faraphel.tasks_valider.utils
import fi.iki.elonen.NanoHTTPD
import java.nio.charset.Charset
/**
* Return the body of a request as a string.
* :param charset: the encoding of the body
*/
fun NanoHTTPD.IHTTPSession.getBody(
charset: Charset = Charset.forName("UTF-8")
): String {
// get the length of the body
val length = this.headers["content-length"]!!.toInt()
// prepare a buffer for the body
val buffer = ByteArray(length)
// read the body into the buffer
this.inputStream.read(buffer, 0, length)
// convert that buffer into a string
return buffer.toString(charset)
}

View file

@ -0,0 +1,8 @@
package com.faraphel.tasks_valider.utils
import java.time.ZoneId
import java.time.format.DateTimeFormatter
var dateTimeFormatter: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss").withZone(ZoneId.systemDefault())