Refactoring of updateOpinions (route/repo/query)

Can nw be set multiple opinion on sigle query
fix OpinionVoter on CREATE
This commit is contained in:
2020-03-22 00:53:08 +01:00
parent 479793503c
commit 589b6f5245
10 changed files with 151 additions and 56 deletions

View File

@@ -1,8 +1,9 @@
package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.entity.Article
import fr.dcproject.entity.OpinionAggregation
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
@@ -44,6 +45,16 @@ open class OpinionChoice(override val requester: Requester) : RepositoryI {
"id" to id
)
/**
* find one opinion choices by id
*/
fun findOpinionChoicesByIds(ids: List<UUID>): List<OpinionChoiceEntity> =
requester
.getFunction("find_opinion_choices_by_ids")
.select(
"ids" to ids
)
fun upsertOpinionChoice(opinionChoice: OpinionChoiceEntity): OpinionChoiceEntity = requester
.getFunction("upsert_opinion_choice")
.selectOne(
@@ -51,20 +62,13 @@ open class OpinionChoice(override val requester: Requester) : RepositoryI {
)!!
}
open class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requester) {
abstract class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requester) {
/**
* Create an Opinion on target (article,...)
*/
fun opinion(opinion: OpinionEntity<T>): OpinionAggregation {
return requester
.getFunction("opinion")
.selectOne(
"reference" to opinion.target.reference,
"target_id" to opinion.target.id,
"opinion" to opinion.id,
"created_by_id" to opinion.createdBy
)!!
}
abstract fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>>
fun updateOpinions(choice: OpinionChoiceRef, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>> =
updateOpinions(listOf(choice), citizen, target)
/**
* Find opinions of one citizen filtered by target ids
@@ -124,13 +128,18 @@ open class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requeste
}
}
class OpinionArticle(requester: Requester) : Opinion<Article>(requester) {
class OpinionArticle(requester: Requester) : Opinion<ArticleRef>(requester) {
/**
* Create an Opinion on Article
* Create an Opinions on Article
*/
fun opinion(opinion: OpinionArticleEntity): OpinionArticleEntity {
override fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionArticleEntity> {
return requester
.getFunction("upsert_opinion")
.selectOne(opinion) ?: error("query 'upsert_opinion' return null")
.getFunction("update_citizen_opinions_by_target_id")
.select(
"choices_ids" to choices.map { it.id },
"citizen_id" to citizen.id,
"target_id" to target.id,
"target_reference" to target.reference
)
}
}

View File

@@ -1,19 +1,16 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.CitizenRef
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.CREATE
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
@@ -47,25 +44,14 @@ object OpinionArticlePaths {
*/
@Location("/articles/{article}/opinions")
@KtorExperimentalAPI
class ArticleOpinion(val article: ArticleEntity) : RequestBuilder<OpinionArticle> {
class ArticleOpinion(val article: ArticleEntity) : RequestBuilder<List<OpinionChoiceRef>> {
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
)
}
private class Content(ids: List<String>) : KoinComponent {
val ids = ids.map { it.toUUID() }
}
override suspend fun getContent(call: ApplicationCall): OpinionArticle {
return call.receive<Content>().create(call.citizen, article)
}
override suspend fun getContent(call: ApplicationCall): List<OpinionChoiceRef> =
call.receive<Content>().ids.map { OpinionChoiceRef(it) }
}
/**
@@ -95,9 +81,9 @@ fun Route.opinionArticle(repo: OpinionArticleRepository) {
put<OpinionArticlePaths.ArticleOpinion> {
call.getContent(it)
.let { opinion ->
assertCan(VIEW, opinion)
repo.opinion(opinion)
.let { choices ->
assertCan(CREATE, it.article)
repo.updateOpinions(choices, citizen, it.article)
}.let {
call.respond(HttpStatusCode.Created, it)
}

View File

@@ -1,5 +1,6 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.ArticleAuthI
import fr.dcproject.entity.Opinion
import io.ktor.application.ApplicationCall
@@ -12,13 +13,17 @@ class OpinionVoter : Voter {
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return (action is Action)
.and(subject is Opinion<*>?)
.and(subject is Opinion<*>? || subject is ArticleAuthI<*>)
}
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
val user = call.user
if (action == Action.CREATE) {
return if (user != null) Vote.GRANTED else Vote.DENIED
return if (user != null && (
(subject is ArticleAuthI<*> && !subject.isDeleted()) ||
(subject is Opinion<*> && subject.createdBy.user.id == user.id)
)) Vote.GRANTED
else Vote.DENIED
}
if (action == Action.VIEW) {

View File

@@ -38,14 +38,15 @@ enum class Vote {
private val votersAttributeKey = AttributeKey<List<Voter>>("voters")
fun ApplicationCall.assertCan(action: ActionI, subject: Any? = null) {
if (!can(action, subject)) {
fun ApplicationCall.assertCan(action: ActionI, subject: Any? = null, agreeIfNullOrEmpty: Boolean = true) {
val isNullOrEmpty = (subject == null || (subject is Collection<*> && subject.isNullOrEmpty()))
if (!can(action, subject) && !agreeIfNullOrEmpty && isNullOrEmpty) {
throw UnauthorizedException(action)
}
}
fun PipelineContext<Unit, ApplicationCall>.assertCan(action: ActionI, subject: Any? = null) =
context.assertCan(action, subject)
fun PipelineContext<Unit, ApplicationCall>.assertCan(action: ActionI, subject: Any? = null, agreeIfNullOrEmpty: Boolean = true) =
context.assertCan(action, subject, agreeIfNullOrEmpty)
fun PipelineContext<Unit, ApplicationCall>.can(action: ActionI, subject: Any? = null) =
context.can(action, subject)

View File

@@ -1376,10 +1376,12 @@ components:
ArticleOpinionRequest:
type: object
properties:
opinion_choice:
type: string
format: uuid
example: 6e978eb5-3c48-0def-b093-e01f43983adb
ids:
type: array
items:
type: string
format: uuid
example: 6e978eb5-3c48-0def-b093-e01f43983adb
OpinionChoices:
description: Opinion Choice

View File

@@ -0,0 +1,11 @@
create or replace function find_opinion_choices_by_ids(_ids uuid[], out resource json)
language plpgsql as
$$
begin
select json_agg(ol) into resource
from opinion_choice ol
where (ol.deleted_at <= now()
or ol.deleted_at is null)
and ol.id = any(_ids);
end;
$$;

View File

@@ -0,0 +1,31 @@
create or replace function update_citizen_opinions_by_target_id(
_choices_ids uuid[],
_citizen_id uuid,
_target_id uuid,
_target_reference regclass,
out opinions json,
out ids_deleted uuid[]
) language plpgsql as
$$
begin
if _target_reference = 'article'::regclass then
insert into opinion_on_article (created_by_id, target_id, choice_id)
select _citizen_id, _target_id, _choice_id
from unnest(_choices_ids) _choice_id
on conflict (created_by_id, target_id, choice_id) do nothing;
with deleted as (
delete from opinion_on_article o
where o.created_by_id = _citizen_id
and o.target_id = _target_id
and (not array[o.choice_id]::uuid[] <@ _choices_ids or _choices_ids = '{}'::uuid[])
returning id
)
select array_agg(d.id) into ids_deleted from deleted d;
else
raise exception '% no implemented for opinion', _target_reference::text;
end if;
select find_citizen_opinions_by_target_id(_citizen_id, _target_id) into opinions;
end
$$;