From ec6e39b1305262c32977f3a9da14bc90df01dfc2 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Tue, 11 Feb 2020 23:03:12 +0100 Subject: [PATCH] Create OpinionChoice --- src/main/kotlin/fr/dcproject/Application.kt | 13 ++++++- src/main/kotlin/fr/dcproject/Module.kt | 2 + .../fr/dcproject/entity/OpinionChoice.kt | 16 ++++++++ .../fr/dcproject/routes/OpinionChoice.kt | 37 +++++++++++++++++++ .../security/voter/OpinionChoiceVoter.kt | 34 +++++++++++++++++ .../opinion/find_opinion_choice_by_id.sql | 15 ++++++++ .../sql/functions/opinion/find_opinions.sql | 8 ++-- src/test/sql/opinion.sql | 8 +++- 8 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/fr/dcproject/entity/OpinionChoice.kt create mode 100644 src/main/kotlin/fr/dcproject/routes/OpinionChoice.kt create mode 100644 src/main/kotlin/fr/dcproject/security/voter/OpinionChoiceVoter.kt create mode 100644 src/main/resources/sql/functions/opinion/find_opinion_choice_by_id.sql diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 96ec84b..70c8622 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -39,6 +39,7 @@ import java.util.concurrent.CompletionException import fr.dcproject.repository.Article as RepositoryArticle import fr.dcproject.repository.Citizen as RepositoryCitizen import fr.dcproject.repository.Constitution as RepositoryConstitution +import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository import fr.dcproject.repository.User as UserRepository fun main(args: Array): Unit = io.ktor.server.jetty.EngineMain.main(args) @@ -121,6 +122,14 @@ fun Application.module(env: Env = PROD) { get().findById(id, true) ?: throw NotFoundException("Citizen $values not found") } } + + convert { + decode { values, _ -> + val id = values.singleOrNull()?.let { UUID.fromString(it) } + ?: throw InternalError("Cannot convert $values to UUID") + get().findOpinionChoiceById(id) ?: throw NotFoundException("OpinionChoice $values not found") + } + } } install(Locations) { @@ -133,7 +142,8 @@ fun Application.module(env: Env = PROD) { CitizenVoter(), CommentVoter(), VoteVoter(), - FollowVoter() + FollowVoter(), + OpinionChoiceVoter() ) } @@ -186,6 +196,7 @@ fun Application.module(env: Env = PROD) { commentConstitution(get()) voteArticle(get(), get(), get()) voteConstitution(get()) + opinionChoice(get()) definition() } } diff --git a/src/main/kotlin/fr/dcproject/Module.kt b/src/main/kotlin/fr/dcproject/Module.kt index b73234b..07c4d7e 100644 --- a/src/main/kotlin/fr/dcproject/Module.kt +++ b/src/main/kotlin/fr/dcproject/Module.kt @@ -15,6 +15,7 @@ import fr.dcproject.repository.CommentGeneric as CommentGenericRepository import fr.dcproject.repository.Constitution as ConstitutionRepository import fr.dcproject.repository.FollowArticle as FollowArticleRepository import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository +import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository import fr.dcproject.repository.User as UserRepository import fr.dcproject.repository.VoteArticle as VoteArticleRepository import fr.dcproject.repository.VoteComment as VoteCommentRepository @@ -57,6 +58,7 @@ val Module = module { single { VoteArticleRepository(get()) } single { VoteConstitutionRepository(get()) } single { VoteCommentRepository(get()) } + single { OpinionChoiceRepository(get()) } single { Migrations(connection = get(), directory = config.sqlFiles) } diff --git a/src/main/kotlin/fr/dcproject/entity/OpinionChoice.kt b/src/main/kotlin/fr/dcproject/entity/OpinionChoice.kt new file mode 100644 index 0000000..5249a89 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/entity/OpinionChoice.kt @@ -0,0 +1,16 @@ +package fr.dcproject.entity + +import fr.postgresjson.entity.immutable.EntityCreatedAt +import fr.postgresjson.entity.immutable.EntityCreatedAtImp +import fr.postgresjson.entity.immutable.UuidEntity +import fr.postgresjson.entity.mutable.EntityDeletedAt +import fr.postgresjson.entity.mutable.EntityDeletedAtImp +import java.util.* + +class OpinionChoice( + id: UUID, + val name: String, + val target: List +) : UuidEntity(id), + EntityCreatedAt by EntityCreatedAtImp(), + EntityDeletedAt by EntityDeletedAtImp() \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/routes/OpinionChoice.kt b/src/main/kotlin/fr/dcproject/routes/OpinionChoice.kt new file mode 100644 index 0000000..a1f096b --- /dev/null +++ b/src/main/kotlin/fr/dcproject/routes/OpinionChoice.kt @@ -0,0 +1,37 @@ +package fr.dcproject.routes + +import fr.dcproject.entity.OpinionChoice +import fr.dcproject.security.voter.OpinionVoter.Action.VIEW +import fr.dcproject.security.voter.assertCan +import io.ktor.application.call +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.Location +import io.ktor.locations.get +import io.ktor.response.respond +import io.ktor.routing.Route +import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository + +@KtorExperimentalLocationsAPI +object OpinionChoicePaths { + @Location("/opinions/{opinionChoice}") + class OpinionChoiceRequest(val opinionChoice: OpinionChoice) + + @Location("/opinions") + class OpinionChoicesRequest(val targets: List) +} + +@KtorExperimentalLocationsAPI +fun Route.opinionChoice(repo: OpinionChoiceRepository) { + get { + assertCan(VIEW, it.opinionChoice) + + call.respond(it.opinionChoice) + } + + get { + val opinions = repo.findOpinionsChoices(it.targets) + assertCan(VIEW, opinions) + + call.respond(opinions) + } +} \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/security/voter/OpinionChoiceVoter.kt b/src/main/kotlin/fr/dcproject/security/voter/OpinionChoiceVoter.kt new file mode 100644 index 0000000..601aab8 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/security/voter/OpinionChoiceVoter.kt @@ -0,0 +1,34 @@ +package fr.dcproject.security.voter + +import fr.dcproject.entity.OpinionChoice +import io.ktor.application.ApplicationCall + +class OpinionChoiceVoter : Voter { + enum class Action : ActionI { + VIEW + } + + override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean { + return (action is Action) + .and(subject is OpinionChoice? || subject is List<*>) + } + + override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote { + if (action == Action.VIEW) { + if (subject is OpinionChoice) { + return Vote.GRANTED + } + if (subject is List<*>) { + subject.forEach { + if (it !is OpinionChoice) { + return Vote.DENIED + } + } + return Vote.GRANTED + } + return Vote.DENIED + } + + return Vote.ABSTAIN + } +} diff --git a/src/main/resources/sql/functions/opinion/find_opinion_choice_by_id.sql b/src/main/resources/sql/functions/opinion/find_opinion_choice_by_id.sql new file mode 100644 index 0000000..7d3c3bb --- /dev/null +++ b/src/main/resources/sql/functions/opinion/find_opinion_choice_by_id.sql @@ -0,0 +1,15 @@ +create or replace function find_opinion_choice_by_id(_id uuid, out resource json) + language plpgsql as +$$ +begin + select to_json(ol) into resource + from opinion_list ol + where (ol.deleted_at <= now() + or ol.deleted_at is null) + and (ol.id = _id); +end; +$$; + +-- drop function if exists find_opinion_choice_by_id(); + +-- select find_opinion_choice_by_id('8c6cb3cc-cac5-93ad-312e-6bd87d9916d9'); \ No newline at end of file diff --git a/src/main/resources/sql/functions/opinion/find_opinions.sql b/src/main/resources/sql/functions/opinion/find_opinions.sql index 333fdcc..bb9528a 100644 --- a/src/main/resources/sql/functions/opinion/find_opinions.sql +++ b/src/main/resources/sql/functions/opinion/find_opinions.sql @@ -1,4 +1,4 @@ -create or replace function find_opinions(out resource json) +create or replace function find_opinion_choices(targets text[] default null, out resource json) language plpgsql as $$ begin @@ -7,8 +7,10 @@ begin from ( select ol.* from opinion_list ol - where ol.deleted_at <= now() - or ol.deleted_at is null + where (ol.deleted_at <= now() + or ol.deleted_at is null) + and (ol.target is null or array_length(targets) = 0 or ol.target = any(targets)) + order by ol.name ) t; end; diff --git a/src/test/sql/opinion.sql b/src/test/sql/opinion.sql index 2f80c01..9fd6ff8 100644 --- a/src/test/sql/opinion.sql +++ b/src/test/sql/opinion.sql @@ -90,8 +90,12 @@ begin 'The first opinion must have a name'; assert( - select find_opinions()#>>'{0, name}' = 'Opinion1' - ), 'find_opinions mst be return all opinions'; + select find_opinion_choices()#>>'{0, name}' = 'Opinion1' + ), 'find_opinion_choices mst be return all opinions'; + + assert( + select (find_opinion_choice_by_id(opinion1)->>'name') = 'Opinion1' + ), 'find_opinion_choice_by_id must return the opinion_choice'; -- delete vote and context delete from opinion;