Implement comment constitution

fix bugs
This commit is contained in:
2019-08-28 23:32:43 +02:00
parent 33f4992b5e
commit 4c2da6ab71
17 changed files with 277 additions and 86 deletions

View File

@@ -12,11 +12,7 @@ import fr.dcproject.entity.Citizen
import fr.dcproject.entity.Constitution
import fr.dcproject.entity.User
import fr.dcproject.routes.*
import fr.dcproject.security.voter.ArticleVoter
import fr.dcproject.security.voter.AuthorizationVoter
import fr.dcproject.security.voter.CitizenVoter
import fr.dcproject.security.voter.CommentVoter
import fr.postgresjson.migration.Migrations
import fr.dcproject.security.voter.*
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
@@ -40,7 +36,6 @@ import java.util.*
import java.util.concurrent.CompletionException
import fr.dcproject.repository.Article as RepositoryArticle
import fr.dcproject.repository.Citizen as RepositoryCitizen
import fr.dcproject.repository.CommentGeneric as CommentGenericRepository
import fr.dcproject.repository.Constitution as RepositoryConstitution
import fr.dcproject.repository.User as UserRepository
@@ -92,14 +87,6 @@ fun Application.module() {
}
}
convert<CommentEntityGeneric> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<CommentGenericRepository>().findById(id) ?: throw InternalError("Comment $values not found")
}
}
convert<Citizen> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
@@ -115,6 +102,7 @@ fun Application.module() {
install(AuthorizationVoter) {
voters = mutableListOf(
ArticleVoter(),
ConstitutionVoter(),
CitizenVoter(),
CommentVoter()
)
@@ -166,6 +154,7 @@ fun Application.module() {
followConstitution(get())
comment(get())
commentArticle(get())
commentConstitution(get())
}
}

View File

@@ -8,6 +8,7 @@ import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
import fr.dcproject.repository.CommentGeneric as CommentGenericRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
@@ -37,8 +38,7 @@ val Module = module {
single { FollowConstitutionRepository(get()) }
single { CommentGenericRepository(get()) }
single { CommentArticleRepository(get()) }
// TODO implment constitution
// single { CommentConstitutionRepository(get()) }
single { CommentConstitutionRepository(get()) }
single { Migrations(connection = get(), directory = config.sqlFiles) }
}

View File

@@ -7,7 +7,7 @@ class Constitution(
id: UUID = UUID.randomUUID(),
var title: String?,
var annonymous: Boolean?,
var titles: List<Title>,
var titles: List<Title> = listOf(),
createdBy: Citizen?
): UuidEntity(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(),

View File

@@ -14,11 +14,7 @@ import fr.dcproject.entity.Constitution as ConstitutionEntity
abstract class Comment <T: UuidEntity>(override var requester: Requester): RepositoryI<CommentEntity<T>> {
override val entityName = CommentEntity::class as KClass<CommentEntity<T>>
open fun findById(id: UUID): CommentEntity<ArticleEntity>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
abstract fun findById(id: UUID): CommentEntity<T>?
abstract fun findByCitizen(
citizen: CitizenEntity,
@@ -81,23 +77,29 @@ abstract class Comment <T: UuidEntity>(override var requester: Requester): Repos
}
fun edit(comment: CommentEntity<T>) {
val reference = comment.target::class.simpleName!!.toLowerCase()
requester
.getFunction("edit_comment")
.sendQuery(
"reference" to reference,
"id" to comment.id,
"content" to comment.content
)
}
}
class CommentGeneric (requester: Requester): Comment<UuidEntity>(requester) {
class GenericTargetEntity(id: UUID = UUID.randomUUID()): UuidEntity(id)
class CommentGeneric (requester: Requester): Comment<GenericTargetEntity>(requester) {
override fun findById(id: UUID): CommentEntity<GenericTargetEntity>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
page: Int,
limit: Int
): Paginated<CommentEntity<UuidEntity>> {
): Paginated<CommentEntity<GenericTargetEntity>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(page, limit,
@@ -108,6 +110,12 @@ class CommentGeneric (requester: Requester): Comment<UuidEntity>(requester) {
}
class CommentArticle (requester: Requester): Comment<ArticleEntity>(requester) {
override fun findById(id: UUID): CommentEntity<ArticleEntity>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
page: Int,
@@ -125,6 +133,12 @@ class CommentArticle (requester: Requester): Comment<ArticleEntity>(requester) {
}
class CommentConstitution (requester: Requester): Comment<ConstitutionEntity>(requester) {
override fun findById(id: UUID): CommentEntity<ConstitutionEntity>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
page: Int,

View File

@@ -33,9 +33,9 @@ class Constitution(override var requester: Requester) : RepositoryI<Constitution
)
}
fun upsert(article: ConstitutionEntity): ConstitutionEntity? {
fun upsert(constitution: ConstitutionEntity): ConstitutionEntity? {
return requester
.getFunction("upsert_constitution")
.selectOne("resource" to article)
.selectOne("resource" to constitution)
}
}

View File

@@ -3,7 +3,6 @@ package fr.dcproject.routes
import fr.dcproject.security.voter.CommentVoter.Action.UPDATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import fr.postgresjson.entity.UuidEntity
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -14,10 +13,8 @@ import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.routing.Route
import java.util.*
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.repository.CommentGeneric as CommentRepository
typealias CommentEntityGeneric = CommentEntity<UuidEntity>
@KtorExperimentalLocationsAPI
object CommentPaths {
// TODO: change UUID by entity converter
@@ -38,7 +35,7 @@ fun Route.comment(repo: CommentRepository) {
assertCan(UPDATE,comment)
comment.content = call.receiveText()
repo.edit(comment as CommentEntity<UuidEntity>)
repo.edit(comment)
call.respond(HttpStatusCode.OK, comment)
}

View File

@@ -11,7 +11,7 @@ import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.entity.Article as ArticleEntity
@@ -37,7 +37,7 @@ fun Route.commentArticle(repo: CommentArticleRepository) {
post<CommentArticlePaths.ArticleCommentRequest> {
assertCan(CREATE, it.article)
val content = call.receive<String>()
val content = call.receiveText()
val comment = CommentEntity(
target = it.article,
createdBy = citizen,

View File

@@ -0,0 +1,55 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.dcproject.security.voter.assertCan
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
@KtorExperimentalLocationsAPI
object CommentConstitutionPaths {
@Location("/constitutions/{constitution}/comments") class ConstitutionCommentRequest(val constitution: ConstitutionEntity)
@Location("/citizens/{citizen}/comments/constitutions") class CitizenCommentConstitutionRequest(val citizen: Citizen)
}
@KtorExperimentalLocationsAPI
fun Route.commentConstitution(repo: CommentConstitutionRepository) {
get<CommentConstitutionPaths.ConstitutionCommentRequest> {
assertCan(VIEW, it.constitution)
val comment = repo.findByTarget(it.constitution)
call.respond(HttpStatusCode.OK, comment)
}
post<CommentConstitutionPaths.ConstitutionCommentRequest> {
assertCan(CREATE, it.constitution)
val content = call.receiveText()
val comment = CommentEntity(
target = it.constitution,
createdBy = citizen,
content = content
)
repo.comment(comment)
call.respond(HttpStatusCode.Created, comment)
}
get<CommentConstitutionPaths.CitizenCommentConstitutionRequest> {
val comments = repo.findByCitizen(it.citizen)
call.respond(comments)
}
}

View File

@@ -0,0 +1,47 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Constitution
import fr.dcproject.entity.User
import io.ktor.application.ApplicationCall
class ConstitutionVoter: Voter {
enum class Action: ActionI {
CREATE,
UPDATE,
VIEW,
DELETE
}
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return (action is Action || action is CommentVoter.Action) && subject is Constitution?
}
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
val user = call.user
if (action == Action.CREATE && user != null) {
return Vote.GRANTED
}
if (action == Action.VIEW) {
return Vote.GRANTED
}
if (action == CommentVoter.Action.CREATE) {
return Vote.GRANTED
}
if (action == CommentVoter.Action.VIEW) {
return Vote.GRANTED
}
if (action == Action.DELETE && user is User && subject is Constitution && subject.createdBy?.userId == user.id) {
return Vote.GRANTED
}
if (action == Action.UPDATE && user is User && subject is Constitution && subject.createdBy?.userId == user.id) {
return Vote.GRANTED
}
return Vote.ABSTAIN
}
}

View File

@@ -3,9 +3,21 @@ create or replace function upsert_article(inout resource json)
$$
declare
new_id uuid;
_id_exist boolean;
begin
insert into article (version_id, created_by_id, title, annonymous, content, description, tags)
-- check if version id already exist
select count(*) >= 1
into _id_exist
from article
where (resource->>'id')::uuid is not null
and id = (resource->>'id')::uuid
-- and draft = false
;
insert into article (id, version_id, created_by_id, title, annonymous, content, description, tags)
select
case when _id_exist then uuid_generate_v4()
else coalesce(id, uuid_generate_v4()) end,
coalesce(version_id, uuid_generate_v4()),
(resource#>>'{created_by, id}')::uuid,
title,

View File

@@ -1,17 +1,13 @@
create or replace function edit_comment(reference regclass, _id uuid, _content text) returns void
create or replace function edit_comment(_id uuid, _content text) returns void
language plpgsql as
$$
begin
if reference = 'article'::regclass then
update comment_on_article c set
update comment c set
"content" = _content
where c.id = _id;
elseif reference = 'constitution'::regclass then
update comment_on_constitution c set
"content" = _content
where c.id = _id;
end if;
end;
$$;
-- drop function if exists edit_comment(regclass, uuid, text);
-- select edit_comment('b0422e48-687f-bea7-b45f-b6b301246e97', 'plop4')

View File

@@ -1,27 +0,0 @@
create or replace function find_comments_article_by_citizen(
_created_by_id uuid,
"limit" int default 50,
"offset" int default 0,
out resource json,
out total int
) language plpgsql as
$$
begin
select json_agg(t), (select count(id) from comment where target_reference = 'article'::regclass)
into resource, total
from (
select
com.*,
find_article_by_id(com.target_id) as target,
find_citizen_by_id(com.created_by_id) as created_by
from comment as com
where created_by_id = _created_by_id
and target_reference = 'article'::regclass
order by created_at desc,
com.created_at desc
limit "limit" offset "offset"
) as t;
end;
$$;
-- drop function if exists find_comments_article_by_citizen(uuid, int, int);

View File

@@ -6,9 +6,23 @@ declare
_title json;
_citizen_id uuid = (resource#>>'{created_by, id}')::uuid;
new_id uuid;
_id_exist boolean;
begin
insert into constitution (version_id, created_by_id, title, annonymous)
-- check if version id already exist
select count(*) >= 1
into _id_exist
from constitution
where (resource->>'id')::uuid is not null
and id = (resource->>'id')::uuid
-- and draft = false
;
raise notice '%', _id_exist;
insert into constitution (id, version_id, created_by_id, title, annonymous)
select
case when _id_exist then uuid_generate_v4()
else coalesce(id, uuid_generate_v4()) end,
version_id,
_citizen_id,
title,

View File

@@ -10,7 +10,7 @@ begin
when 'article'::regclass then
find_article_by_id(_id)
when 'constitution'::regclass then
find_article_by_id(_id)
find_constitution_by_id(_id)
else
json_build_object('id', _id)
end
@@ -19,3 +19,5 @@ end;
$$;
-- drop function if exists find_reference_by_id(uuid, regclass, out json);
-- select find_reference_by_id('8944221c-3766-f952-7064-9f229c288049'::uuid, 'constitution'::regclass)

View File

@@ -0,0 +1,62 @@
package feature
import fr.dcproject.entity.Citizen
import io.cucumber.java8.En
import org.joda.time.DateTime
import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import java.util.concurrent.CompletionException
import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Constitution.Title as TitleEntity
import fr.dcproject.entity.User as UserEntity
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository
class ConstitutionSteps: En, KoinTest {
init {
Given("I have constitution with id {string}") { id: String ->
var citizen = Citizen(
name = Citizen.Name("John", "Doe"),
birthday = DateTime.now(),
user = UserEntity(username = "john-doe", plainPassword = "azerty")
)
try {
get<CitizenRepository>().insertWithUser(citizen)
} catch (e: CompletionException) {
citizen = get<CitizenRepository>().findByUsername("john-doe")!!
}
val title1 = TitleEntity(
name = "My Title"
)
val constitution = ConstitutionEntity(
id = UUID.fromString(id),
title = "hello",
titles = listOf(title1),
createdBy = citizen,
annonymous = false
)
get<ConstitutionRepository>().upsert(constitution)
}
Given("I have constitution with id {string} created by {string}") { id: String, username: String ->
val citizen = get<CitizenRepository>().findByUsername(username)!!
val title1 = TitleEntity(
name = "My Title"
)
val constitution = ConstitutionEntity(
id = UUID.fromString(id),
title = "hello",
titles = listOf(title1),
createdBy = citizen,
annonymous = false
)
get<ConstitutionRepository>().upsert(constitution)
}
}
}

View File

@@ -1,6 +1,5 @@
Feature: comment Article and Constitution
Feature: comment Article
# Article
Scenario: The route for comment article must response a 201 and return object
Given I am authenticated as John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd"
And I have article with id "9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b"
@@ -34,7 +33,3 @@ Feature: comment Article and Constitution
Then the response status code should be 200
And the JSON should contain:
| content | Hello boy |
# Constitution
# TODO

View File

@@ -0,0 +1,35 @@
Feature: comment Constitution
Scenario: The route for comment constitution must response a 201 and return object
Given I am authenticated as John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd"
And I have constitution with id "9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b"
When I send a POST request to "/constitutions/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/comments" with body:
"""
Hello mister
"""
Then the response status code should be 201
Scenario: The route for get comments of constitutions for the current citizen must response a 200 and return objects
Given I have citizen John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd"
And I have constitution with id "9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b"
When I send a GET request to "/citizens/64b7b379-2298-43ec-b428-ba134930cabd/comments/constitutions"
Then the response status code should be 200
And the response should contain object:
| current_page | 1 |
| limit | 50 |
Scenario: The route for edit comment must response a 200 and return object
Given I am authenticated as username 3 with id "92877af7-0a45-fd6a-2ed7-fe81e1236b78"
When I send a PUT request to "/comments/b0422e48-687f-bea7-b45f-b6b301246e97" with body:
"""
Hello boy
"""
Then the response status code should be 200
And the JSON should contain:
| content | Hello boy |
Scenario: The route for get comment must response a 200 and return object
When I send a GET request to "/comments/b0422e48-687f-bea7-b45f-b6b301246e97"
Then the response status code should be 200
And the JSON should contain:
| content | Hello boy |