[WIP] implemented better UI, implemented validating student tasks and fixed an issue in the HEAD, POST and DELETE methods of the REST api

This commit is contained in:
Faraphel 2024-06-09 12:28:48 +02:00
parent 7545e6aca9
commit e73477b626
20 changed files with 733 additions and 248 deletions

View file

@ -11,7 +11,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.faraphel.tasks_valider" applicationId = "com.faraphel.tasks_valider"
minSdk = 24 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -52,8 +52,8 @@ android {
} }
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.13.0") implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
implementation("androidx.activity:activity-compose:1.9.0") implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform("androidx.compose:compose-bom:2023.08.00")) implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")

View file

@ -5,7 +5,7 @@
<!-- SDK --> <!-- SDK -->
<uses-sdk <uses-sdk
android:minSdkVersion="24" android:minSdkVersion="26"
tools:ignore="GradleOverrides" /> tools:ignore="GradleOverrides" />
<!-- Permissions --> <!-- Permissions -->

View file

@ -5,6 +5,8 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/** /**
@ -32,7 +34,7 @@ class TaskClient(
this.cookies.addAll(cookies) this.cookies.addAll(cookies)
} }
} }
) ).callTimeout(30.seconds)
.build() .build()
// TODO(Faraphel): automatically convert content to the correct type ? // TODO(Faraphel): automatically convert content to the correct type ?

View file

@ -1,14 +1,68 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.ClassEntity import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class ClassApi(dao: BaseTaskDao<ClassEntity>, session: SessionEntity) : class ClassApi(private val dao: BaseTaskDao<ClassEntity>, private val session: SessionEntity) : BaseApi {
BaseTaskApi<ClassEntity>( companion object {
dao, private val parser = Gson() ///< The JSON parser
object: TypeToken<ClassEntity>() {}, }
session
// Requests
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, ClassEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, ClassEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, ClassEntity::class.java)
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,14 +1,68 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.PersonEntity import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class PersonApi(dao: BaseTaskDao<PersonEntity>, session: SessionEntity) : class PersonApi(private val dao: BaseTaskDao<PersonEntity>, private val session: SessionEntity) : BaseApi {
BaseTaskApi<PersonEntity>( companion object {
dao, private val parser = Gson() ///< The JSON parser
object: TypeToken<PersonEntity>() {}, }
session
// Requests
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, PersonEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, PersonEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, PersonEntity::class.java)
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,15 +1,72 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class RelationClassPersonApi(dao: BaseTaskDao<RelationClassPersonEntity>, session: SessionEntity) : class RelationClassPersonApi(
BaseTaskApi<RelationClassPersonEntity>( private val dao: BaseTaskDao<RelationClassPersonEntity>,
dao, private val session: SessionEntity
object: TypeToken<RelationClassPersonEntity>() {}, ) : BaseApi {
session companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
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, RelationClassPersonEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, RelationClassPersonEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, RelationClassPersonEntity::class.java)
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,15 +1,72 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class RelationPersonSessionSubjectApi(dao: BaseTaskDao<RelationPersonSessionSubjectEntity>, session: SessionEntity) : class RelationPersonSessionSubjectApi(
BaseTaskApi<RelationPersonSessionSubjectEntity>( private val dao: BaseTaskDao<RelationPersonSessionSubjectEntity>,
dao, private val session: SessionEntity
object: TypeToken<RelationPersonSessionSubjectEntity>() {}, ) : BaseApi {
session companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
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, RelationPersonSessionSubjectEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, RelationPersonSessionSubjectEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, RelationPersonSessionSubjectEntity::class.java)
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,13 +1,71 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class SessionApi(dao: BaseTaskDao<SessionEntity>, session: SessionEntity) :
BaseTaskApi<SessionEntity>( class SessionApi(
dao, private val dao: BaseTaskDao<SessionEntity>,
object: TypeToken<SessionEntity>() {}, private val session: SessionEntity
session ) : BaseApi {
companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
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, SessionEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, SessionEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, SessionEntity::class.java)
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,15 +1,72 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.SubjectEntity import com.faraphel.tasks_valider.database.entities.SubjectEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class SubjectApi(dao: BaseTaskDao<SubjectEntity>, session: SessionEntity) : class SubjectApi(
BaseTaskApi<SubjectEntity>( private val dao: BaseTaskDao<SubjectEntity>,
dao, private val session: SessionEntity
object: TypeToken<SubjectEntity>() {}, ) : BaseApi {
session companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
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, SubjectEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, SubjectEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, SubjectEntity::class.java)
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,15 +1,72 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.TaskEntity import com.faraphel.tasks_valider.database.entities.TaskEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class TaskApi(dao: BaseTaskDao<TaskEntity>, session: SessionEntity) : class TaskApi(
BaseTaskApi<TaskEntity>( private val dao: BaseTaskDao<TaskEntity>,
dao, private val session: SessionEntity
object: TypeToken<TaskEntity>() {}, ) : BaseApi {
session companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
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, TaskEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, TaskEntity::class.java)
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, TaskEntity::class.java)
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,15 +1,74 @@
package com.faraphel.tasks_valider.database.api.entities package com.faraphel.tasks_valider.database.api.entities
import com.faraphel.tasks_valider.database.api.entities.base.BaseTaskApi import com.faraphel.tasks_valider.database.api.entities.base.BaseApi
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.ValidationEntity import com.faraphel.tasks_valider.database.entities.ValidationEntity
import com.google.gson.reflect.TypeToken import com.faraphel.tasks_valider.utils.getBody
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
class ValidationApi(dao: BaseTaskDao<ValidationEntity>, session: SessionEntity) : class ValidationApi(
BaseTaskApi<ValidationEntity>( private val dao: BaseTaskDao<ValidationEntity>,
dao, private val session: SessionEntity
object: TypeToken<ValidationEntity>() {}, ) : BaseApi {
session companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
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, ValidationEntity::class.java)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
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, ValidationEntity::class.java)
// save the data into the database
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
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, ValidationEntity::class.java)
// delete the object from the database
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,80 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities.base
import android.util.Log
import com.faraphel.tasks_valider.database.dao.base.BaseCronDao
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.Gson
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: BaseCronDao<Entity>,
private val entityTypeToken: TypeToken<Entity>,
) : BaseApi {
companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
override fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val obj = parser.fromJson<Entity>(
httpSession.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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAll())
)
}
override fun post(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val data = httpSession.inputStream.bufferedReader().readText()
Log.i("post data", "data raw : $data")
val obj = parser.fromJson<Entity>(
data,
this.entityTypeToken.type
)
Log.i("post data", "data parsed : $obj")
val id = this.dao.insert(obj)
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.CREATED,
"text/plain",
id.toString()
)
}
override fun delete(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val obj = parser.fromJson<Entity>(
httpSession.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,51 +0,0 @@
package com.faraphel.tasks_valider.database.api.entities.base
import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao
import com.faraphel.tasks_valider.database.entities.SessionEntity
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 data for Task objects.
* @param Entity the entity type to handle
*/
abstract class BaseTaskApi<Entity: BaseEntity>(
private val dao: BaseTaskDao<Entity>,
private val entityTypeToken: TypeToken<Entity>,
private val session: SessionEntity,
) : BaseJsonApi<Entity>(
dao,
entityTypeToken,
) {
companion object {
private val parser = Gson() ///< The JSON parser
}
// Requests
override fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
val obj = parser.fromJson<Entity>(
httpSession.inputStream.bufferedReader().readText(),
this.entityTypeToken.type
)
// check if the object is in the object accessible from the session
val exists = this.dao.getAllBySession(session.id).contains(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(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"application/json",
parser.toJson(this.dao.getAllBySession(session.id))
)
}
}

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.ui.screen.authentification package com.faraphel.tasks_valider.ui.screen.authentification
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
@ -8,6 +8,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.database.entities.PersonEntity import com.faraphel.tasks_valider.database.entities.PersonEntity
@ -20,19 +24,37 @@ fun AuthentificationServerScreen(personEntity: MutableState<PersonEntity?>) {
val firstName = remember { mutableStateOf("") } val firstName = remember { mutableStateOf("") }
val lastName = remember { mutableStateOf("") } val lastName = remember { mutableStateOf("") }
Column { 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 // first name
TextField( TextField(
value = firstName.value, value = firstName.value,
placeholder = { Text("first name") },
onValueChange = { text -> firstName.value = text }, onValueChange = { text -> firstName.value = text },
) )
// last name // last name
TextField( TextField(
value = lastName.value, value = lastName.value,
placeholder = { Text("last name") },
onValueChange = { text -> lastName.value = text }, onValueChange = { text -> lastName.value = text },
) )
// separator
Spacer(modifier = Modifier.height(24.dp))
// confirm button // confirm button
Button(onClick = { Button(onClick = {
// create the person entity with the given information // create the person entity with the given information

View file

@ -3,10 +3,14 @@ package com.faraphel.tasks_valider.ui.screen.communication.internet
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -29,7 +33,20 @@ fun CommunicationInternetSelectScreen(activity: Activity, database: TaskDatabase
@Composable @Composable
fun CommunicationInternetSelectContent(controller: NavController) { 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 // client mode
Button(onClick = { controller.navigate("client") }) { Text("Client") } Button(onClick = { controller.navigate("client") }) { Text("Client") }
// server mode // server mode

View file

@ -4,8 +4,7 @@ import android.app.Activity
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@ -13,7 +12,14 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -93,16 +99,34 @@ fun CommunicationInternetServerContent(
Thread { refreshClasses(database, classes) }.start() Thread { refreshClasses(database, classes) }.start()
} }
Column { 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 // 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 }) { Button(onClick = { areClassesExpanded.value = !areClassesExpanded.value }) {
Row {
Text(text = "Class")
// display the selected class, if selected // display the selected class, if selected
if (selectedClass.value != null) if (selectedClass.value != null) Text(text = selectedClass.value!!.name)
Text(text = selectedClass.value!!.name) else Text(text = "<Not selected>")
}
} }
// class selector
DropdownMenu( DropdownMenu(
expanded = areClassesExpanded.value, expanded = areClassesExpanded.value,
onDismissRequest = { areClassesExpanded.value = false } onDismissRequest = { areClassesExpanded.value = false }
@ -118,11 +142,20 @@ fun CommunicationInternetServerContent(
) )
} }
} }
}
// server port // server port
Row(verticalAlignment = Alignment.CenterVertically) {
// descriptor
Text(text = "Port")
// separator
Spacer(modifier = Modifier.width(width = 12.dp))
// input
TextField( TextField(
modifier = Modifier.width(80.dp),
value = serverPort.intValue.toString(), value = serverPort.intValue.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
onValueChange = { text -> onValueChange = { text ->
val port = text.toInt() val port = text.toInt()
if (port in RANGE_SERVER_PORT) { if (port in RANGE_SERVER_PORT) {
@ -130,6 +163,7 @@ fun CommunicationInternetServerContent(
} }
} }
) )
}
// check if a class is selected // check if a class is selected
if (selectedClass.value != null) if (selectedClass.value != null)

View file

@ -4,12 +4,17 @@ import android.app.Activity
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -58,7 +63,20 @@ fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase)
fun CommunicationSelectContent(controller: NavController, activity: Activity) { fun CommunicationSelectContent(controller: NavController, activity: Activity) {
val isWifiP2pSupported = BwfManager.isSupported(activity) val isWifiP2pSupported = BwfManager.isSupported(activity)
Column { 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))
// internet communication mode // internet communication mode
Button(onClick = { controller.navigate("internet") }) { Button(onClick = { controller.navigate("internet") }) {
Text("Internet") Text("Internet")
@ -68,7 +86,7 @@ fun CommunicationSelectContent(controller: NavController, activity: Activity) {
Button( Button(
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
// if the WiFi-Direct is not supported, the button is grayed out // 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 = { onClick = {
// if the WiFi-Direct is supported, navigate to the WiFi-Direct screen // if the WiFi-Direct is supported, navigate to the WiFi-Direct screen

View file

@ -4,13 +4,17 @@ import android.app.Activity
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.TaskClient import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.database.entities.PersonEntity import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.google.gson.Gson import com.google.gson.Gson
@ -44,10 +48,25 @@ fun TaskSessionScreen(
selectedStudent.value!! selectedStudent.value!!
) )
Column { 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 // if the groups have already been defined, display them
for (student in students.value!!) { for (student in students.value!!) {
Button(onClick = { selectedStudent.value = student }) { Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp, horizontal = 16.dp),
onClick = { selectedStudent.value = student },
) {
Text(text = student.fullName()) Text(text = student.fullName())
} }
} }

View file

@ -5,8 +5,7 @@ import android.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -14,6 +13,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.TaskClient
import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
import com.faraphel.tasks_valider.database.entities.* import com.faraphel.tasks_valider.database.entities.*
@ -25,7 +29,6 @@ import java.time.Instant
* This screen represent a student * This screen represent a student
* @param student the student object * @param student the student object
*/ */
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun TaskStudentScreen( fun TaskStudentScreen(
activity: Activity, activity: Activity,
@ -47,8 +50,17 @@ fun TaskStudentScreen(
) )
}.start() }.start()
Column { Column(
Text(text = student.fullName()) 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 both the list of tasks and validations are loaded
if (!(tasks.value == null || validations.value == null)) { if (!(tasks.value == null || validations.value == null)) {
@ -56,22 +68,31 @@ fun TaskStudentScreen(
// get the validation // get the validation
val validation = validations.value!!.firstOrNull { validation -> validation.taskId == task.id } val validation = validations.value!!.firstOrNull { validation -> validation.taskId == task.id }
Button(onClick = {}) { Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 64.dp),
) {
Row { Row {
Column { Column {
// task title // task title
Text(task.title) Text(text = task.title, fontWeight = FontWeight.Bold)
// task description // task description
task.description?.let { description -> Text(description) } task.description?.let { description -> Text(description) }
// if the task have been validated, show the date // if the task have been validated, show the date
if (validation != null) Text(validation.date.toString()) if (validation != null) Text(validation.date.toString())
} }
// separator
Spacer(modifier = Modifier.fillMaxWidth())
// the validation state // the validation state
Checkbox( Checkbox(
checked = validation != null, checked = validation != null,
enabled = user.role.permissions.contains(TaskPermission.WRITE), enabled = user.role.permissions.contains(TaskPermission.WRITE),
onCheckedChange = { state -> onCheckedChange = { state ->
Thread { Thread {
// TODO(Faraphel): simplify or put the UI refresh in the update function ?
// send a notification to the server about the validation
updateValidation( updateValidation(
client, client,
state, state,
@ -82,6 +103,14 @@ fun TaskStudentScreen(
task.id, task.id,
) )
) )
// refresh the UI
refreshTasksValidations(
activity,
client,
student,
tasks,
validations
)
}.start() }.start()
} }
) )

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)
}