Continue to implement opinion

improve target reference
Improve Tests for Opinion
fix SQL:upsert_opinion
This commit is contained in:
2020-02-14 01:26:47 +01:00
parent 60bd24e653
commit 471013984c
42 changed files with 683 additions and 137 deletions

View File

@@ -197,6 +197,7 @@ fun Application.module(env: Env = PROD) {
commentConstitution(get())
voteArticle(get(), get(), get())
voteConstitution(get())
opinionArticle(get())
opinionChoice(get())
definition()
}
@@ -215,6 +216,9 @@ fun Application.module(env: Env = PROD) {
exception<NotFoundException> { e ->
call.respond(HttpStatusCode.BadRequest, e.message!!)
}
exception<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden)
}
}
install(CORS) {

View File

@@ -32,8 +32,6 @@ open class Comment<T : TargetI>(
target = parent.target,
content = content
)
override val reference get() = TargetI.getReference(this)
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id)

View File

@@ -6,7 +6,7 @@ import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.immutable.UuidEntityI
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI> :
UuidEntityI,
@@ -26,16 +26,18 @@ interface TargetI : UuidEntityI {
enum class TargetName(val targetReference: String) {
Article("article"),
Constitution("constitution"),
Comment("comment")
Comment("comment"),
Opinion("opinion")
}
companion object {
fun <T : TargetI> getReference(t: KClass<T>): String {
return when {
t.isSuperclassOf(Article::class) -> TargetName.Article.targetReference
t.isSuperclassOf(Constitution::class) -> TargetName.Constitution.targetReference
t.isSuperclassOf(Comment::class) -> TargetName.Comment.targetReference
else -> throw error("target not implemented")
t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
t.isSubclassOf(Opinion::class) -> TargetName.Opinion.targetReference
else -> throw error("target not implemented: ${t.qualifiedName}")
}
}

View File

@@ -1,6 +1,9 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.EntityCreatedBy
import fr.postgresjson.entity.immutable.EntityCreatedByImp
import java.util.*
open class Opinion<T : TargetI>(
@@ -9,11 +12,16 @@ open class Opinion<T : TargetI>(
override val target: T,
val choice: OpinionChoice
) : ExtraI<T>,
UuidEntity(id),
TargetRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
fun getName(): String = choice.name
}
typealias OpinionArticle = Opinion<Article>
class OpinionArticle(
id: UUID = UUID.randomUUID(),
createdBy: CitizenBasic,
target: ArticleRef,
choice: OpinionChoice
) : Opinion<ArticleRef>(id, createdBy, target, choice)

View File

@@ -10,7 +10,7 @@ import java.util.*
class OpinionChoice(
id: UUID,
val name: String,
val target: List<String>
val target: List<String>?
) : OpinionChoiceRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp()

View File

@@ -3,13 +3,13 @@ package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
class OpinionAggregation(
override val entries: Set<Map.Entry<String, Int>> = emptySet()
) : AbstractMap<String, Int>(), EntityI
private val underlying: MutableMap<String, Any> = mutableMapOf()
) : MutableMap<String, Any> by underlying, EntityI
interface Opinionable {
val opinions: MutableMap<String, Int>
var opinions: MutableMap<String, Int>
}
class OpinionableImp : Opinionable {
override val opinions: MutableMap<String, Int> = mutableMapOf()
override var opinions: MutableMap<String, Int> = mutableMapOf()
}

View File

@@ -1,27 +0,0 @@
package fr.dcproject.entity.request
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.OpinionArticle
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.TargetRef
import fr.dcproject.repository.Article
import fr.dcproject.repository.OpinionChoice
import fr.dcproject.utils.toUUID
import org.koin.core.KoinComponent
import org.koin.core.get
class ArticleOpinionRequest(
target: String,
opinionChoice: String
) : RequestBuilderWithCreator<Citizen, OpinionArticle>, KoinComponent {
val target = TargetRef(target.toUUID())
val opinionChoice = OpinionChoiceRef(opinionChoice.toUUID())
override fun create(citizen: Citizen): OpinionArticle {
return OpinionArticle(
choice = get<OpinionChoice>().findOpinionChoiceById(opinionChoice.id)!!,
target = get<Article>().findById(target.id)!!,
createdBy = citizen
)
}
}

View File

@@ -1,14 +1,11 @@
package fr.dcproject.entity.request
import fr.dcproject.entity.CitizenRef
import fr.postgresjson.entity.EntityI
import io.ktor.application.ApplicationCall
interface Request
interface RequestBuilder<E: EntityI> : Request {
fun create(): E
interface RequestBuilder<E> {
suspend fun getContent(call: ApplicationCall): E
}
interface RequestBuilderWithCreator<C: CitizenRef, E: EntityI> : Request {
fun create(citizen: C): E
}
suspend fun <E> ApplicationCall.getContent(builder: RequestBuilder<E>) = builder.getContent(this)

View File

@@ -11,6 +11,7 @@ import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Opinion as OpinionEntity
import fr.dcproject.entity.OpinionArticle as OpinionArticleEntity
import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity
open class OpinionChoice(override val requester: Requester) : RepositoryI {
@@ -25,6 +26,14 @@ open class OpinionChoice(override val requester: Requester) : RepositoryI {
"targets" to targets
)
/**
* find opinion choices by name
*/
fun findOpinionsChoiceByName(name: String): OpinionChoiceEntity? =
findOpinionsChoices().first {
it.name == name
}
/**
* find one opinion choices by id
*/
@@ -104,9 +113,18 @@ open class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requeste
.select(page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"citizen" to citizen.id
"citizen_id" to citizen.id
)
}
}
class OpinionArticle(requester: Requester) : Opinion<Article>(requester)
class OpinionArticle(requester: Requester) : Opinion<Article>(requester) {
/**
* Create an Opinion on Article
*/
fun opinion(opinion: OpinionArticleEntity): OpinionArticleEntity {
return requester
.getFunction("upsert_opinion")
.selectOne(opinion) ?: error("query 'upsert_opinion' return null")
}
}

View File

@@ -1,11 +1,18 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.request.ArticleOpinionRequest
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.OpinionArticle
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.request.RequestBuilder
import fr.dcproject.entity.request.getContent
import fr.dcproject.repository.OpinionChoice
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.dcproject.utils.toUUID
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
@@ -17,6 +24,7 @@ import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
import org.koin.core.KoinComponent
import org.koin.core.get
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@@ -24,7 +32,7 @@ import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object OpinionArticlePaths {
/**
* Get paginated opinion of citizen for one article
* Get paginated opinions of citizen for all articles
*/
@Location("/citizens/{citizen}/opinions/articles")
class CitizenOpinionArticleRequest(
@@ -36,16 +44,37 @@ object OpinionArticlePaths {
/**
* Put an opinion on one article
*/
@Location("/articles/{article}/opinons")
class ArticleOpinion(val article: ArticleEntity)
@Location("/articles/{article}/opinions")
@KtorExperimentalAPI
class ArticleOpinion(val article: ArticleEntity) : RequestBuilder<OpinionArticle> {
private class Content(
opinionChoice: String
) : KoinComponent {
val opinionChoice = OpinionChoiceRef(opinionChoice.toUUID())
fun create(citizen: Citizen, article: ArticleEntity): OpinionArticle {
return OpinionArticle(
choice = get<OpinionChoice>().findOpinionChoiceById(opinionChoice.id) ?: throw BadRequestException("OpinionChoice not exist: id(${opinionChoice.id})"),
target = article,
createdBy = citizen
)
}
}
override suspend fun getContent(call: ApplicationCall): OpinionArticle {
return call.receive<Content>().create(call.citizen, article)
}
}
/**
* Get all Opinion of citizen on targets by target ids
*/
@Location("/citizen/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>): KoinComponent {
@Location("/citizens/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>) : KoinComponent {
val id: List<UUID> = id.toUUID()
val opinionsEntities = get<OpinionArticleRepository>()
.findCitizenOpinionsByTargets(citizen, id.toUUID())
.findCitizenOpinionsByTargets(citizen, this.id)
}
}
@@ -64,9 +93,12 @@ fun Route.opinionArticle(repo: OpinionArticleRepository) {
}
put<OpinionArticlePaths.ArticleOpinion> {
val optionArticle = call.receive<ArticleOpinionRequest>().create(citizen)
assertCan(VIEW, optionArticle)
call.respond(HttpStatusCode.Created, optionArticle)
call.getContent(it)
.let { opinion ->
assertCan(VIEW, opinion)
repo.opinion(opinion)
}.let {
call.respond(HttpStatusCode.Created, it)
}
}
}

View File

@@ -1,7 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.entity.OpinionChoice
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
import fr.dcproject.security.voter.OpinionChoiceVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -17,7 +17,7 @@ object OpinionChoicePaths {
class OpinionChoiceRequest(val opinionChoice: OpinionChoice)
@Location("/opinions")
class OpinionChoicesRequest(val targets: List<String>)
class OpinionChoicesRequest(val targets: List<String> = emptyList())
}
@KtorExperimentalLocationsAPI

View File

@@ -37,9 +37,10 @@ class OpinionVoter : Voter {
}
if (action == Action.DELETE) {
return if (subject is Opinion<*>
&& user != null
&& subject.createdBy.user.id == user.id)
return if (subject is Opinion<*> &&
user != null &&
subject.createdBy.user.id == user.id
)
Vote.GRANTED
else Vote.DENIED
}