Refactors Articles and Voter

- Move files into components (article)
- Split articles routes
- Refactoring for remove ktor-voter (ArticleVoter)
- Remove mutability
- Move DataConversion to separate file (Converter.kt)
- Add Schemas for Articles routes
- Fix SQL Query for Workgroup roles
- rename container_name in docker-compose
This commit is contained in:
2021-01-14 11:23:27 +01:00
parent 03401f711e
commit a1c1accc87
124 changed files with 2026 additions and 1828 deletions

2
.env
View File

@@ -1,4 +1,4 @@
NAME=dc-project
APP_NAME=dc-project
DATABASE_URL=jdbc:postgresql:dc-project

View File

@@ -2,28 +2,10 @@
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<PostgresCodeStyleSettings version="2">
<option name="myVersion" value="2" />
<PostgresCodeStyleSettings version="5">
<option name="USE_GENERAL_STYLE" value="false" />
<option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_CASE" value="1" />

View File

@@ -3,12 +3,6 @@
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" />
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="fr.dcproject.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" />

View File

@@ -2,12 +2,6 @@
<configuration default="false" name="Voter Tests" type="JUnit" factoryName="JUnit" show_console_on_std_err="true">
<output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="fr.dcproject.security.voter.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />

View File

@@ -15,12 +15,12 @@ help: ## This help.
bd: build-docker
build-docker: ## Build the docker image of application
build-docker: ## Build the docker image of application (alias: bd)
docker build -t dc-project -f docker/app/Dockerfile .
pd: publish-docker
publish-docker: build-docker ## Publish docker image of application to Github
publish-docker: build-docker ## Publish docker image of application to Github (alias: pd)
@git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1)
@cat ./GH_TOKEN.txt | docker login docker.pkg.github.com -u ${GITHUB_USERNAME} --password-stdin
@docker tag dc-project docker.pkg.github.com/flecomte/dc-project/dc-project:${VERSION}
@@ -28,27 +28,32 @@ publish-docker: build-docker ## Publish docker image of application to Github
rd: run-docker
run-docker: ## Build and Run all docker services
run-docker: ## Build and Run all docker services (alias: rd)
docker-compose up -d --build
rdd: run-docker-dependencies
run-docker-dependencies: ## Build and Run dependencies docker services (alias: rdd)
docker-compose up -d --build openapi rabbitmq redis elasticsearch db
pm: publish-maven
publish-maven: ## Publish JAR file to Github
publish-maven: ## Publish JAR file to Github (alias: pm)
@git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1)
gradlew publish
f: fixtures
fixtures: ## Import fixtures
fixtures: ## Import fixtures (alias: f)
bash src/main/resources/sql/fixtures/fixtures.sh
reset-database: ## Import fixtures
reset-database: ## Reset database !!!
cd src/main/resources/sql/ ; bash resetDB.sh
test-sql: ## Test sql
cd src/test/sql/ ; bash test.sh 1
v: vertion
v: version
vertion: ## Show current version
version: ## Show current version (alias: v)
@echo ${VERSION}

View File

@@ -23,6 +23,7 @@ version = versioning.info.run {
plugins {
jacoco
application
maven
id("maven-publish")
id("org.jetbrains.kotlin.jvm") version "1.3.50"
@@ -138,8 +139,8 @@ dependencies {
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
implementation("com.auth0:java-jwt:3.8.2")
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7")
implementation("com.github.flecomte:postgres-json:1.2.1")
implementation("com.github.flecomte:ktor-voter:2.2.1")
implementation("com.github.flecomte:postgres-json:2.0.0")
implementation("com.github.flecomte:ktor-voter:3.0.0")
implementation("com.sendgrid:sendgrid-java:4.4.1")
implementation("io.lettuce:lettuce-core:5.2.2.RELEASE")
implementation("com.rabbitmq:amqp-client:5.8.0")

98
doc/schema/Article.puml Normal file
View File

@@ -0,0 +1,98 @@
@startuml
title Search / Get articles
actor Front
box Article API
control Controller
control Repository
entity Article
database Postgres
endbox
box View System
control ArticleViewManager
database Elasticsearch
endbox
box Notification System
control EventNotification
database RabbitMQ
database Redis
endbox
Front -> Controller++: GET /articles?page=1
Controller -> Repository++: find
Repository -> Postgres++: find_articles()
return
return
return: 200, Articles
newpage Create / Update Article
Front -> Controller: POST /article
activate Controller
Controller -> Controller: Convert dto to Entity
Controller -> Controller: Check Authorization
alt Authorize
Controller -> Repository++: upsert(entity)
Repository -> Postgres++: upsert_article
return
return
Controller -> Controller: Convert to dto
Front <-- Controller: 200, New Article
else not authorize
Front <-- Controller: 403, "Forbidden"
end
Controller -> EventNotification: raiseEvent(ArticleUpdate)
deactivate Controller
activate EventNotification
EventNotification ->> RabbitMQ
deactivate EventNotification
...
RabbitMQ -->> EventNotification++
EventNotification ->> : Send Email
EventNotification ->> Redis : Push Event Notification
return <<ACK>>
newpage get one article by id
Front -> Controller: GET /article/{article}
activate Controller
Controller -> Repository++: findById()
Repository -> Postgres++: find_article_by_id()
return
return
Controller -> Controller: Check Authorization
alt Authorize
Controller -> ArticleViewManager++: getViewsCount(Article)
ArticleViewManager -> Elasticsearch++
return
return
Controller -> Controller: Convert Article and Views to dto
Front <<-- Controller: 200, Article
else not authorize
Front <<-- Controller: 403, "Forbidden"
end
Controller -> ArticleViewManager++: increment the view counter
ArticleViewManager -> Elasticsearch++
return
return
deactivate Controller
newpage get article versions by id
Front -> Controller: GET /articles/{article}/versions
activate Controller
Controller -> Controller: Check Authorization
alt Authorize
Controller -> Repository++: findVersionsByVersionId
Repository -> Postgres++: find_articles_versions_by_version_id
return
return
Controller -> Controller: Convert to dto
Front <-- Controller: 200, Articles versions
else not authorize
Front <-- Controller: 403, "Forbidden"
end
deactivate Controller
@enduml

View File

@@ -3,13 +3,13 @@
version: '3.7'
services:
sonarqube:
container_name: sonarqube_${NAME}
container_name: ${APP_NAME}_sonarqube
image: sonarqube
ports:
- ${SONARQUBE_PORT}:9000
openapi:
container_name: openapi_${NAME}
container_name: ${APP_NAME}_openapi
image: swaggerapi/swagger-ui
ports:
- ${OPENAPI_PORT}:8080
@@ -17,14 +17,14 @@ services:
URL: "http://localhost:8080"
rabbitmq:
container_name: rabbitmq_${NAME}
container_name: ${APP_NAME}_rabbitmq
image: rabbitmq:management-alpine
ports:
- ${RABBITMQ_PORT}:5672
- ${RABBITMQ_MANAGEMENT_PORT}:15672
redis:
container_name: redis_${NAME}
container_name: ${APP_NAME}_redis
image: redis:6.0-rc-alpine
ports:
- ${REDIS_PORT}:6379
@@ -32,7 +32,7 @@ services:
- redis-data:/var/lib/redis:rw
app:
container_name: app_${NAME}
container_name: ${APP_NAME}_app
build:
context: .
dockerfile: docker/app/Dockerfile
@@ -51,7 +51,7 @@ services:
- rabbitmq
elasticsearch:
container_name: elasticsearch_${NAME}
container_name: ${APP_NAME}_elasticsearch
image: elasticsearch:6.7.1
ports:
- ${ELASTIC_REST}:9200
@@ -63,7 +63,7 @@ services:
retries: 20
db:
container_name: postgresql_${NAME}
container_name: ${APP_NAME}_postgresql
build:
context: docker/postgresql
ports:

View File

@@ -4,6 +4,7 @@ COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck --no-daemon
RUN gradle shadowJar
#### RUN ####
FROM adoptopenjdk/openjdk11:jre-11.0.4_11-alpine

View File

@@ -8,36 +8,33 @@ import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import fr.dcproject.Env.PROD
import fr.dcproject.component.article.route.findArticleVersions
import fr.dcproject.component.article.route.upsertArticle
import fr.dcproject.component.article.routes.findArticles
import fr.dcproject.component.article.routes.getOneArticle
import fr.dcproject.elasticsearch.configElasticIndexes
import fr.dcproject.entity.*
import fr.dcproject.entity.User
import fr.dcproject.event.EventNotification
import fr.dcproject.event.EventSubscriber
import fr.dcproject.routes.*
import fr.dcproject.security.voter.*
import fr.ktorVoter.AuthorizationVoter
import fr.ktorVoter.ForbiddenException
import fr.ktorVoter.VoterException
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.auth.Authentication
import io.ktor.auth.authenticate
import io.ktor.auth.jwt.jwt
import io.ktor.client.HttpClient
import io.ktor.client.engine.jetty.Jetty
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.auth.jwt.*
import io.ktor.client.*
import io.ktor.client.engine.jetty.*
import io.ktor.features.*
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.auth.HttpAuthHeader
import io.ktor.jackson.jackson
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Locations
import io.ktor.response.respond
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import io.ktor.http.*
import io.ktor.http.auth.*
import io.ktor.jackson.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
import io.ktor.websocket.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog
import org.koin.core.qualifier.named
@@ -47,13 +44,7 @@ import org.slf4j.event.Level
import java.time.Duration
import java.util.*
import java.util.concurrent.CompletionException
import fr.dcproject.entity.Workgroup as WorkgroupEntity
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
import fr.dcproject.repository.Workgroup as WorkgroupRepository
fun main(args: Array<String>): Unit = io.ktor.server.jetty.EngineMain.main(args)
@@ -66,117 +57,19 @@ enum class Env { PROD, TEST, CUCUMBER }
fun Application.module(env: Env = PROD) {
install(Koin) {
Slf4jLog()
modules(Module)
modules(KoinModule)
}
install(CallLogging) {
level = Level.INFO
}
install(DataConversion) {
convert<UUID> {
decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) }
}
install(DataConversion, converters)
encode { value ->
when (value) {
null -> listOf()
is UUID -> listOf(value.toString())
else -> throw InternalError("Cannot convert $value as UUID")
}
}
}
// TODO: create generic convert for entityI
convert<Article> {
decode { values, _ ->
values.singleOrNull()?.let {
get<RepositoryArticle>().findById(UUID.fromString(it))
?: throw NotFoundException("Article $values not found")
} ?: throw NotFoundException("Article $values not found")
}
}
convert<ArticleRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ArticleRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Article""")
}
}
convert<CommentRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CommentRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Comment""")
}
}
convert<ConstitutionRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ConstitutionRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Constitution""")
}
}
convert<Constitution> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<RepositoryConstitution>().findById(id) ?: throw NotFoundException("Constitution $values not found")
}
}
convert<Citizen> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<RepositoryCitizen>().findById(id) ?: throw NotFoundException("Citizen $values not found")
}
}
convert<CitizenRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CitizenRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Citizen""")
}
}
convert<OpinionChoice> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<OpinionChoiceRepository>().findOpinionChoiceById(id)
?: throw NotFoundException("OpinionChoice $values not found")
}
}
convert<WorkgroupRef> {
decode { values, _ ->
values.singleOrNull()?.let {
WorkgroupRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Workgroup""")
}
}
convert<WorkgroupEntity> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<WorkgroupRepository>().findById(id)
?: throw NotFoundException("Workgroup $values not found")
}
}
}
install(Locations) {
}
install(Locations)
install(AuthorizationVoter) {
voters = listOf(
ArticleVoter(get()),
ConstitutionVoter(),
CitizenVoter(),
CommentVoter(),
@@ -255,10 +148,13 @@ fun Application.module(env: Env = PROD) {
}
}
install(Routing) {
install(Routing.Feature) {
// trace { application.log.trace(it.buildText()) }
authenticate(optional = true) {
article(get(), get())
findArticles(get(), get())
getOneArticle(get(), get())
upsertArticle(get(), get(), get())
findArticleVersions(get(), get())
auth(get(), get(), get())
citizen(get(), get())
constitution(get())
@@ -293,6 +189,10 @@ fun Application.module(env: Env = PROD) {
exception<NotFoundException> { e ->
call.respond(HttpStatusCode.NotFound, e.message!!)
}
exception<VoterException> {
if (call.user == null) call.respond(HttpStatusCode.Unauthorized)
else call.respond(HttpStatusCode.Forbidden)
}
exception<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden)
}

View File

@@ -2,20 +2,21 @@ package fr.dcproject
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.ktorVoter.ForbiddenException
import io.ktor.application.ApplicationCall
import io.ktor.auth.authentication
import io.ktor.util.AttributeKey
import io.ktor.util.pipeline.PipelineContext
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import org.koin.core.context.GlobalContext
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.Citizen as CitizenRepository
class ForbiddenException(message: String) : Exception(message)
private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) {
val user = authentication.principal<UserI>() ?: throw ForbiddenException()
val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected")
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
}

View File

@@ -0,0 +1,118 @@
package fr.dcproject
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.entity.*
import fr.dcproject.repository.OpinionChoice
import fr.dcproject.repository.Workgroup
import io.ktor.features.*
import io.ktor.util.*
import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import java.util.*
private typealias ConverterDeclaration = DataConversion.Configuration.() -> Unit
private inline fun <reified T> DataConversion.Configuration.get(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T = GlobalContext.get().koin.rootScope.get(qualifier, parameters)
@KtorExperimentalAPI
val converters: ConverterDeclaration = {
convert<UUID> {
decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) }
}
encode { value ->
when (value) {
null -> listOf()
is UUID -> listOf(value.toString())
else -> throw InternalError("Cannot convert $value as UUID")
}
}
}
convert<ArticleForView> {
decode { values, _ ->
values.singleOrNull()?.let {
get<ArticleRepository>().findById(UUID.fromString(it))
?: throw NotFoundException("Article $values not found")
} ?: throw NotFoundException("Article $values not found")
}
}
convert<ArticleRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ArticleRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Article""")
}
}
convert<CommentRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CommentRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Comment""")
}
}
convert<ConstitutionRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ConstitutionRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Constitution""")
}
}
convert<Constitution> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<fr.dcproject.repository.Constitution>().findById(id) ?: throw NotFoundException("Constitution $values not found")
}
}
convert<Citizen> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<fr.dcproject.repository.Citizen>().findById(id) ?: throw NotFoundException("Citizen $values not found")
}
}
convert<CitizenRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CitizenRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Citizen""")
}
}
convert<fr.dcproject.entity.OpinionChoice> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<OpinionChoice>().findOpinionChoiceById(id)
?: throw NotFoundException("OpinionChoice $values not found")
}
}
convert<WorkgroupRef> {
decode { values, _ ->
values.singleOrNull()?.let {
WorkgroupRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Workgroup""")
}
}
convert<fr.dcproject.entity.Workgroup<CitizenBasic>> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<Workgroup>().findById(id)
?: throw NotFoundException("Workgroup $values not found")
}
}
}

View File

@@ -8,11 +8,13 @@ import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.event.publisher.Publisher
import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.messages.SsoManager
import fr.dcproject.views.ArticleViewManager
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations
@@ -25,7 +27,6 @@ import org.apache.http.HttpHost
import org.elasticsearch.client.RestClient
import org.koin.core.qualifier.named
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
@@ -42,7 +43,7 @@ import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
@KtorExperimentalAPI
val Module = module {
val KoinModule = module {
single { Config }
@@ -114,6 +115,9 @@ val Module = module {
single { OpinionArticleRepository(get()) }
single { WorkgroupRepository(get()) }
// Voters
single { ArticleVoter(get()) }
// Elasticsearch Client
single<RestClient> {
RestClient.builder(

View File

@@ -0,0 +1,126 @@
package fr.dcproject.component.article
import fr.dcproject.entity.*
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*
data class ArticleForView (
override val id: UUID = UUID.randomUUID(),
override val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
override val createdBy: CitizenRef,
override val versionNumber: Int = 0,
override val versionId: UUID = UUID.randomUUID(),
val workgroup: WorkgroupSimple<CitizenRef>? = null,
override val opinions: Opinions = emptyMap(),
override val draft: Boolean = false,
override val deletedAt: DateTime? = null
) : ArticleRef(id),
ArticleAuthI<CitizenRef>,
ArticleWithTitleI,
EntityVersioning<UUID, Int>,
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(deletedAt),
ArticleRefVersioningI,
Opinionable,
Votable by VotableImp() {
val lastVersion: Boolean = false
}
interface ArticleForUpdateI<C: CitizenRef> : ArticleI, ArticleWithTitleI, VersionableRef, TargetI, CreatedBy<C> {
val anonymous: Boolean
val content: String
val description: String
val draft: Boolean
val workgroup: WorkgroupRef?
}
class ArticleForUpdate (
id: UUID? = null,
override val title: String,
override val anonymous: Boolean = true,
override val content: String,
override val description: String,
tags: List<String> = emptyList(),
override val draft: Boolean = false,
override val createdBy: CitizenRef,
override val workgroup: WorkgroupRef? = null,
versionId: UUID? = null,
override val deletedAt: DateTime? = null
) : ArticleForUpdateI<CitizenRef>,
ArticleAuthI<CitizenRef>,
ArticleRefVersioningI by ArticleRefVersioningImmutable(id, versionId = versionId ?: UUID.randomUUID()) {
val tags: List<String> = tags.distinct()
val isNew = versionId == null
}
@Deprecated("")
open class ArticleSimple(
id: UUID = UUID.randomUUID(),
var title: String,
override val createdBy: CitizenBasic,
override var draft: Boolean = false,
var workgroup: WorkgroupSimple<CitizenRef>? = null
) : ArticleAuthI<CitizenBasicI>,
ArticleRefVersioning(id),
EntityCreatedAt by EntityCreatedAtImp(),
CreatedBy<CitizenBasicI> by CreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(),
Opinionable by OpinionableImp()
class ArticleForListing(
id: UUID? = null,
override val title: String,
override val createdBy: CitizenCart,
override val workgroup: WorkgroupCart?,
override val deletedAt: DateTime?,
override val draft: Boolean
) : ArticleForListingI,
ArticleRef(id),
ArticleAuthI<CitizenCartI>,
Votable by VotableImp(),
CreatedBy<CitizenCartI>
interface ArticleForListingI : ArticleWithTitleI, CreatedBy<CitizenCartI> {
val workgroup: WorkgroupCartI?
}
@Deprecated("", ReplaceWith("ArticleRefVersioningImmutable"))
open class ArticleRefVersioning(
id: UUID = UUID.randomUUID(),
override var versionNumber: Int = 0,
versionId: UUID = UUID.randomUUID()
) : ArticleRefVersioningI,
ArticleRef(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionNumber, versionId)
open class ArticleRefVersioningImmutable(
id: UUID? = null,
versionId: UUID = UUID.randomUUID()
) : ArticleRefVersioningI,
ArticleRef(id),
VersionableRef by VersionableRefImp(versionId)
interface ArticleRefVersioningI : ArticleI, VersionableRef
open class ArticleRef(
id: UUID? = null
) : ArticleI, TargetRef(id)
interface ArticleI : UuidEntityI, TargetI
interface ArticleWithTitleI : ArticleI {
val title: String
}
interface ArticleAuthI<U : CitizenI> :
ArticleI,
CreatedBy<U>,
EntityDeletedAt {
val draft: Boolean
}

View File

@@ -1,23 +1,19 @@
package fr.dcproject.repository
package fr.dcproject.component.article
import fr.dcproject.entity.ArticleForUpdate
import fr.dcproject.entity.ArticleSimple
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
class Article(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): ArticleEntity? {
class ArticleRepository(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): ArticleForView? {
val function = requester.getFunction("find_article_by_id")
return function.selectOne("id" to id)
}
fun findVerionsByVersionsId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleEntity> {
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForView> {
return requester
.getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to versionId)
@@ -27,10 +23,10 @@ class Article(override var requester: Requester) : RepositoryI {
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
direction: RepositoryI.Direction? = null,
search: String? = null,
filter: Filter = Filter()
): Paginated<ArticleSimple> {
): Paginated<ArticleForListing> {
return requester
.getFunction("find_articles")
.select(
@@ -42,7 +38,7 @@ class Article(override var requester: Requester) : RepositoryI {
)
}
fun upsert(article: ArticleForUpdate): ArticleEntity? {
fun upsert(article: ArticleForUpdate): ArticleForView? {
return requester
.getFunction("upsert_article")
.selectOne("resource" to article)
@@ -52,4 +48,4 @@ class Article(override var requester: Requester) : RepositoryI {
val createdById: String? = null,
val workgroupId: String? = null
) : Parameter
}
}

View File

@@ -1,17 +1,25 @@
package fr.dcproject.views
package fr.dcproject.component.article
import fr.dcproject.entity.*
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.ViewAggregation
import fr.dcproject.utils.contentToString
import fr.dcproject.utils.getJsonField
import fr.dcproject.utils.toIso
import fr.dcproject.views.ViewManager
import org.elasticsearch.client.Request
import org.elasticsearch.client.Response
import org.elasticsearch.client.RestClient
import org.joda.time.DateTime
import java.util.*
class ArticleViewManager(private val restClient: RestClient) : ViewManager<ArticleRefVersioning> {
override fun addView(ip: String, article: ArticleRefVersioning, citizen: CitizenRef?, dateTime: DateTime): Response? {
/**
* Wrapper for manage views with elasticsearch
*/
class ArticleViewManager(private val restClient: RestClient) : ViewManager<ArticleRefVersioningI> {
/**
* Add view on article to elasticsearch
*/
override fun addView(ip: String, article: ArticleRefVersioningI, citizen: CitizenI?, dateTime: DateTime): Response? {
val isLogged = (citizen != null).toString()
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
val request = Request(
@@ -36,7 +44,10 @@ class ArticleViewManager(private val restClient: RestClient) : ViewManager<Artic
return restClient.performRequest(request)
}
override fun getViewsCount(article: ArticleRefVersioning): ViewAggregation {
/**
* Get article views aggregations from elasticsearch
*/
override fun getViewsCount(article: ArticleRefVersioningI): ViewAggregation {
val request = Request(
"GET",
"/views/_search"

View File

@@ -0,0 +1,49 @@
package fr.dcproject.component.article
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.CreatedBy
import fr.dcproject.entity.VersionableRef
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
class ArticleVoter(private val articleRepo: ArticleRepository): Voter() {
fun <S: ArticleAuthI<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
fun <S: ArticleAuthI<*>> canView(subject: S, citizen: CitizenI?): VoterResponse {
return if (subject.isDeleted()) denied("Article is deleted", "article.deleted")
else if (subject.draft && (citizen == null || subject.createdBy.id != citizen.id)) denied("Article is draft, but it's not yours", "article.draft.not.yours")
else granted()
}
fun <S: CreatedBy<*>> canDelete(subject: S, citizen: CitizenI?): VoterResponse {
if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected")
return if (subject.createdBy.id == citizen.id) {
granted()
} else {
denied("Cannot delete article if is not yours", "article.delete.notYours")
}
}
fun <S> canUpsert(subject: S, citizen: CitizenI?): VoterResponse
where S: ArticleI,
S: CreatedBy<*>,
S: VersionableRef {
if (citizen == null) return denied("You must be connected to create article", "article.create.notConnected")
/* The new Article must by created by the same citizen of the connected citizen */
if (subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */
val lastVersionId = articleRepo
.findVersionsByVersionId(1, 1, subject.versionId)
.result
.firstOrNull()?.createdBy?.id
return when (lastVersionId) {
null -> granted("You can create a new Article")
citizen.id -> granted("Last version is yours")
else -> denied("Last version is not yours", "article.lastVersion.notYours")
}
}
return denied("This article must be yours for update it", "article.update.notYours")
}
}

View File

@@ -0,0 +1,39 @@
package fr.dcproject.component.article.route
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.voter.assert
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
@KtorExperimentalLocationsAPI
@Location("/articles/{article}/versions")
class ArticleVersionsRequest(
val article: ArticleForView,
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@KtorExperimentalLocationsAPI
private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) =
findVersionsByVersionId(request.page, request.limit, request.article.versionId)
@KtorExperimentalLocationsAPI
fun Route.findArticleVersions(repo: ArticleRepository, voter: ArticleVoter) {
get<ArticleVersionsRequest> {
repo.findVersions(it)
.apply { voter.assert { canView(it.article, citizenOrNull) } }
.let { call.respond(it) }
}
}

View File

@@ -0,0 +1,46 @@
package fr.dcproject.component.article.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForListing
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.voter.assert
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
@Location("/articles")
class ArticlesRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null,
val createdBy: String? = null,
val workgroup: String? = null
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> {
return find(
request.page,
request.limit,
request.sort,
request.direction,
request.search,
ArticleRepository.Filter(createdById = request.createdBy, workgroupId = request.workgroup)
)
}
fun Route.findArticles (repo: ArticleRepository, voter: ArticleVoter) {
get<ArticlesRequest> {
repo.findArticles(it)
.apply { voter.assert { canView(result, citizenOrNull) } }
.let { call.respond(it) }
}
}

View File

@@ -0,0 +1,69 @@
package fr.dcproject.component.article.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.component.article.routes.ArticleRequest.Output
import fr.dcproject.dto.*
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
import kotlinx.coroutines.launch
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.*
@KtorExperimentalLocationsAPI
@Location("/articles/{articleId}")
class ArticleRequest(val articleId: UUID) : KoinComponent {
val repo: ArticleRepository by inject()
@KtorExperimentalAPI
val article: ArticleForView = repo.findById(articleId) ?: throw NotFoundException("Article $articleId not found")
class Output(
article: ArticleForView,
views: fr.dcproject.entity.ViewAggregation = fr.dcproject.entity.ViewAggregation()
) : CreatedAt by CreatedAt.Imp(article),
Opinionable by Opinionable.Imp(article),
Votable by Votable.Imp(article),
Versionable by Versionable.Imp(article),
Viewable by Viewable.Imp(views) {
val id = article.id
val title = article.title
val anonymous = article.anonymous
val content = article.content
val description = article.description
val tags = article.tags
val draft = article.draft
val lastVersion = article.lastVersion
val createdBy = article.createdBy
val workgroup = article.workgroup // TODO change to workgroup DTO
}
}
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) {
get<ArticleRequest> {
voter.assert { canView(it.article, citizenOrNull) }
Output(
it.article,
viewManager.getViewsCount(it.article)
).also { out ->
call.respond(out)
}
launch {
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull)
}
}
}

View File

@@ -0,0 +1,66 @@
package fr.dcproject.component.article.route
import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForUpdate
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.entity.WorkgroupRef
import fr.dcproject.event.ArticleUpdate
import fr.dcproject.event.raiseEvent
import fr.dcproject.repository.Workgroup
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.pipeline.*
import java.util.*
@KtorExperimentalLocationsAPI
@Location("/articles")
class PostArticleRequest {
class Input(
val id: UUID?,
val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
val draft: Boolean = false,
val versionId: UUID?,
val workgroup: WorkgroupRef? = null
)
}
@KtorExperimentalLocationsAPI
fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: Workgroup, voter: ArticleVoter) {
suspend fun PipelineContext<Unit, ApplicationCall>.convertDtoToEntity(): ArticleForUpdate = call.receive<PostArticleRequest.Input>().run {
ArticleForUpdate(
id = id ?: UUID.randomUUID(),
title = title,
anonymous = anonymous,
content = content,
description = description,
tags = tags,
draft = draft,
createdBy = call.citizen,
workgroup = if (workgroup != null) workgroupRepository.findById(workgroup.id) else null,
versionId = versionId
)
}
post<PostArticleRequest> {
val article = convertDtoToEntity()
voter.assert { canUpsert(article, citizenOrNull) }
val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated")
call.respond(newArticle)
raiseEvent(ArticleUpdate.event, ArticleUpdate(newArticle))
}
}

View File

@@ -0,0 +1,12 @@
package fr.dcproject.dto
import fr.postgresjson.entity.EntityCreatedAt
import org.joda.time.DateTime
interface CreatedAt {
val createdAt: DateTime
class Imp(parent: EntityCreatedAt) : CreatedAt {
override val createdAt: DateTime = parent.createdAt
}
}

View File

@@ -0,0 +1,11 @@
package fr.dcproject.dto
typealias Opinions = Map<String, Int>
interface Opinionable {
val opinions: Opinions
class Imp(parent: fr.dcproject.entity.Opinionable): Opinionable {
override val opinions: Opinions = parent.opinions
}
}

View File

@@ -0,0 +1,15 @@
package fr.dcproject.dto
import fr.postgresjson.entity.EntityVersioning
import java.util.*
interface Versionable {
val versionId: UUID
val versionNumber: Int
class Imp(parent: EntityVersioning<UUID, Int>) : Versionable {
override val versionNumber: Int = parent.versionNumber
override val versionId: UUID = parent.versionId
}
}

View File

@@ -0,0 +1,10 @@
package fr.dcproject.dto
import fr.dcproject.entity.ViewAggregation
class ViewAggregation(
val total: Int,
val unique: Int
) {
constructor(views: ViewAggregation) : this(views.total, views.unique)
}

View File

@@ -0,0 +1,9 @@
package fr.dcproject.dto
interface Viewable {
var views: ViewAggregation
class Imp(views: fr.dcproject.entity.ViewAggregation) : Viewable {
override var views: ViewAggregation = ViewAggregation(views.total, views.unique)
}
}

View File

@@ -0,0 +1,9 @@
package fr.dcproject.dto
interface Votable {
val votes: VoteAggregation
class Imp(parent: fr.dcproject.entity.Votable): Votable {
override val votes: VoteAggregation = VoteAggregation(parent)
}
}

View File

@@ -0,0 +1,11 @@
package fr.dcproject.dto
import fr.dcproject.entity.Votable
class VoteAggregation(parent: Votable) {
val up: Int = parent.votes.up
val neutral: Int = parent.votes.neutral
val down: Int = parent.votes.down
val total: Int = parent.votes.total
val score: Int = parent.votes.score
}

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory
fun waitElasticsearchIsUp(client: RestClient) {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch")
val request = Request("GET", "/_cluster/health")
repeat(40) {
repeat(5*60/2) { // 5 minutes
runCatching {
client.performRequest(request).statusLine.statusCode
}.onSuccess {

View File

@@ -1,114 +0,0 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import fr.postgresjson.entity.mutable.EntityVersioning
import fr.postgresjson.entity.mutable.UuidEntityVersioning
import java.util.*
class Article(
id: UUID? = null,
title: String,
override var anonymous: Boolean = true,
override var content: String,
override var description: String,
override var tags: List<String> = emptyList(),
draft: Boolean = false,
override var lastVersion: Boolean = false,
override val createdBy: CitizenBasic,
workgroup: WorkgroupSimple<CitizenRef>? = null
) : ArticleFull,
ArticleForUpdateI,
ArticleAuthI<CitizenBasicI>,
ArticleSimple(id, title, createdBy, draft, workgroup),
Viewable by ViewableImp() {
init {
tags = tags.distinct()
}
}
interface ArticleForUpdateI: ArticleI, EntityVersioning<UUID, Int>, TargetI {
val title: String
val anonymous: Boolean
val content: String
val description: String
val draft: Boolean
val createdBy: CitizenRef
val workgroup: WorkgroupRef?
}
class ArticleForUpdate(
id: UUID?,
override val title: String,
override val anonymous: Boolean = true,
override val content: String,
override val description: String,
tags: List<String> = emptyList(),
override val draft: Boolean = false,
override val createdBy: CitizenRef,
override val workgroup: WorkgroupRef? = null,
versionId: UUID?
) : ArticleForUpdateI,
ArticleRefVersioning(id, versionId = versionId ?: UUID.randomUUID()) {
val tags: List<String> = tags.distinct()
val isNew = versionId == null
}
open class ArticleSimple(
id: UUID? = null,
override var title: String,
override val createdBy: CitizenBasic,
override var draft: Boolean = false,
override var workgroup: WorkgroupSimple<CitizenRef>? = null
) : ArticleSimpleI,
ArticleAuthI<CitizenBasicI>,
ArticleRefVersioning(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(),
Opinionable by OpinionableImp()
open class ArticleRefVersioning(
id: UUID? = null,
versionNumber: Int? = null,
versionId: UUID = UUID.randomUUID()
) : ArticleRef(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionNumber, versionId)
open class ArticleRef(
id: UUID? = null
) : ArticleI, TargetRef(id)
interface ArticleI : UuidEntityI, TargetI
interface ArticleSimpleI :
ArticleI,
EntityVersioning<UUID, Int>,
EntityCreatedBy<CitizenBasicI>,
EntityCreatedAt,
EntityDeletedAt,
Votable {
var title: String
var workgroup: WorkgroupSimple<CitizenRef>?
}
interface ArticleBasicI :
ArticleSimpleI {
var anonymous: Boolean
var content: String
var description: String
var tags: List<String>
}
interface ArticleFull :
ArticleBasicI {
var draft: Boolean
var lastVersion: Boolean
}
interface ArticleAuthI<U : CitizenWithUserI> :
ArticleI,
EntityCreatedBy<U>,
EntityDeletedAt {
var draft: Boolean
}

View File

@@ -1,26 +1,26 @@
package fr.dcproject.entity
import fr.dcproject.entity.CitizenI.Name
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.immutable.UuidEntityI
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*
@Deprecated("")
class Citizen(
id: UUID = UUID.randomUUID(),
name: Name,
email: String,
birthday: DateTime,
voteAnonymous: Boolean = true,
followAnonymous: Boolean = true,
override val user: User
override val id: UUID = UUID.randomUUID(),
override val name: Name,
override val email: String,
override val birthday: DateTime,
override val voteAnonymous: Boolean = true,
override val followAnonymous: Boolean = true,
override val user: User,
deletedAt: DateTime? = null
) : CitizenFull,
CitizenBasic(id, name, email, birthday, voteAnonymous, followAnonymous, user),
EntityCreatedAt by EntityCreatedAtImp() {
CitizenBasicI,
CitizenRef(id),
CitizenCartI,
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(deletedAt) {
var workgroups: List<WorkgroupAndRoles> = emptyList()
class WorkgroupAndRoles(
@@ -29,29 +29,43 @@ class Citizen(
)
}
open class CitizenBasic(
id: UUID = UUID.randomUUID(),
name: Name,
@Deprecated("")
data class CitizenBasic(
override var id: UUID = UUID.randomUUID(),
override var name: Name,
override var email: String,
override var birthday: DateTime,
override var voteAnonymous: Boolean = true,
override var followAnonymous: Boolean = true,
override val user: User
override val user: User,
override val deletedAt: DateTime? = null
) : CitizenBasicI,
CitizenSimple(id, name, user)
CitizenRefWithUser(id, user),
EntityDeletedAt by EntityDeletedAtImp(deletedAt)
@Deprecated("")
open class CitizenSimple(
id: UUID = UUID.randomUUID(),
var name: Name,
user: UserRef
) : CitizenRefWithUser(id, user)
class CitizenCart(
id: UUID = UUID.randomUUID(),
override val name: Name,
override val user: UserRef
) : CitizenRef(id),
CitizenCartI
interface CitizenCartI : CitizenI, CitizenWithUserI {
val name: Name
}
open class CitizenRefWithUser(
id: UUID = UUID.randomUUID(),
override val user: UserRef
) : CitizenWithUserI,
CitizenRef(id),
EntityDeletedAt by EntityDeletedAtImp()
CitizenRef(id)
open class CitizenRef(
id: UUID = UUID.randomUUID()
@@ -60,22 +74,29 @@ open class CitizenRef(
interface CitizenI : UuidEntityI {
data class Name(
var firstName: String,
var lastName: String,
var civility: String? = null
) {
override val firstName: String,
override val lastName: String,
override val civility: String? = null
) : NameI
interface NameI {
val firstName: String
val lastName: String
val civility: String?
fun getFullName(): String = "${civility ?: ""} $firstName $lastName".trim()
}
}
@Deprecated("")
interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt {
var name: Name
var email: String
var birthday: DateTime
var voteAnonymous: Boolean
var followAnonymous: Boolean
val name: Name
val email: String
val birthday: DateTime
val voteAnonymous: Boolean
val followAnonymous: Boolean
}
@Deprecated("")
interface CitizenFull : CitizenBasicI {
override val user: User
}

View File

@@ -1,30 +1,28 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*
open class Comment<T : TargetI>(
class CommentForView<T : TargetI, C : CitizenRef>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override var target: T,
var content: String,
val responses: List<Comment<T>>? = null,
var parent: Comment<T>? = null,
val parentsIds: List<UUID>? = null,
val childrenCount: Int? = null
) : ExtraI<T, CitizenBasicI>,
CommentRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
override val createdBy: C,
override val target: T,
override var content: String,
override val parent: CommentParent<T>? = null,
val childrenCount: Int? = null,
override val deletedAt: DateTime? = null
) : ExtraI<T, C>,
CommentForUpdate<T, C>(id, createdBy, target, content, parent, deletedAt),
CommentWithTargetI<T>,
EntityCreatedBy<C> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(),
TargetI {
constructor(
createdBy: CitizenBasic,
parent: Comment<T>,
createdBy: C,
parent: CommentParent<T>,
content: String
) : this(
createdBy = createdBy,
@@ -34,6 +32,43 @@ open class Comment<T : TargetI>(
)
}
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id)
open class CommentForUpdate<T : TargetI, C: CitizenRef>(
override val id: UUID = UUID.randomUUID(),
override val createdBy: C,
override val target: T,
open var content: String,
open val parent: CommentParent<T>? = null,
override val deletedAt: DateTime? = null
) : CommentParent<T>(id, deletedAt, target),
ExtraI<T, C>,
CommentWithTargetI<T>,
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C>,
EntityDeletedAt,
TargetI {
constructor(
createdBy: C,
parent: CommentParent<T>,
content: String
) : this(
createdBy = createdBy,
parent = parent,
target = parent.target,
content = content
)
}
sealed class CommentS(id: UUID) : TargetRef(id)
open class CommentParent<T: TargetI>(
override val id: UUID,
override val deletedAt: DateTime?,
override val target: T
) : CommentRef(id),
CommentParentI<T>
interface CommentParentI<T: TargetI> : CommentI, EntityDeletedAt, CommentWithTargetI<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T>
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
interface CommentI : EntityI

View File

@@ -1,10 +1,8 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import fr.postgresjson.entity.mutable.EntityVersioning
import fr.postgresjson.entity.mutable.UuidEntityVersioning
import fr.dcproject.component.article.ArticleI
import fr.dcproject.component.article.ArticleSimple
import fr.postgresjson.entity.*
import java.util.*
class Constitution(
@@ -33,17 +31,17 @@ class Constitution(
) : ConstitutionSimple.TitleSimple<ArticleSimple>(id, name, rank)
}
open class ConstitutionSimple<Cr : CitizenRefWithUser, T : ConstitutionSimple.TitleSimple<*>>(
open class ConstitutionSimple<Cr : CitizenWithUserI, T : ConstitutionSimple.TitleSimple<*>>(
id: UUID = UUID.randomUUID(),
var title: String,
var anonymous: Boolean = true,
open var titles: MutableList<T> = mutableListOf(),
var draft: Boolean = false,
var lastVersion: Boolean = false,
val title: String,
val anonymous: Boolean = true,
val titles: MutableList<T> = mutableListOf(),
val draft: Boolean = false,
val lastVersion: Boolean = false,
override val createdBy: Cr,
versionId: UUID = UUID.randomUUID()
) : ConstitutionRef(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionId = versionId),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionId = versionId, versionNumber = 0),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<Cr> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp() {

View File

@@ -0,0 +1,13 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityI
/**
* TODO remove EntityCreatedBy<EntityI>
*/
interface CreatedBy<T : CitizenI> : EntityCreatedBy<EntityI> {
override val createdBy: T
}
class CreatedByImp<T : CitizenI>(override val createdBy: T) : CreatedBy<T>

View File

@@ -0,0 +1,8 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
import java.util.*
interface EntityI : EntityI {
val id: UUID
}

View File

@@ -1,17 +1,21 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt
import fr.postgresjson.entity.immutable.EntityCreatedBy
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.immutable.UuidEntityI
import fr.dcproject.component.article.ArticleRef
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.entity.UuidEntityI
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI, C : CitizenI> :
UuidEntityI,
AsTarget<T>,
EntityCreatedAt,
EntityCreatedBy<C> {
EntityCreatedBy<C>
interface AsTarget<T: TargetI> {
val target: T
}

View File

@@ -1,8 +1,9 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.*
import java.util.*
@Deprecated("")
class Follow<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
@@ -10,11 +11,26 @@ class Follow<T : TargetI>(
) : ExtraI<T, CitizenBasicI>,
FollowSimple<T, CitizenBasicI>(id, createdBy, target)
@Deprecated("")
open class FollowSimple<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(),
override val createdBy: C,
override var target: T
) : ExtraI<T, C>,
UuidEntity(id),
FollowRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C> by EntityCreatedByImp(createdBy)
class FollowForUpdate<T: TargetI, C: CitizenI>(
id: UUID = UUID.randomUUID(),
override val target: T,
override val createdBy: C
) : FollowRef(id),
AsTarget<T>,
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
open class FollowRef(
override val id: UUID
) : FollowI
interface FollowI: UuidEntityI

View File

@@ -1,11 +1,10 @@
package fr.dcproject.entity
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 fr.dcproject.component.article.ArticleRef
import fr.postgresjson.entity.*
import java.util.*
@Deprecated("")
open class Opinion<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
@@ -19,9 +18,24 @@ open class Opinion<T : TargetI>(
fun getName(): String = choice.name
}
@Deprecated("")
class OpinionArticle(
id: UUID = UUID.randomUUID(),
createdBy: CitizenBasic,
target: ArticleRef,
choice: OpinionChoice
) : Opinion<ArticleRef>(id, createdBy, target, choice)
) : Opinion<ArticleRef>(id, createdBy, target, choice)
data class OpinionForUpdate<T: TargetI>(
override val id: UUID = UUID.randomUUID(),
val target: T,
val choice: OpinionChoice,
override val createdBy: CitizenRef
) : OpinionRef(id),
EntityCreatedBy<CitizenI> by EntityCreatedByImp(createdBy)
open class OpinionRef(
override val id: UUID
) : OpinionI
interface OpinionI: UuidEntityI

View File

@@ -1,10 +1,6 @@
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 fr.postgresjson.entity.*
import java.util.*
class OpinionChoice(

View File

@@ -1,15 +1,12 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
class OpinionAggregation(
private val underlying: MutableMap<String, Any> = mutableMapOf()
) : MutableMap<String, Any> by underlying, EntityI
typealias Opinions = Map<String, Int>
typealias OpinionsMutable = MutableMap<String, Int>
interface Opinionable {
var opinions: MutableMap<String, Int>
val opinions: Opinions
}
class OpinionableImp : Opinionable {
override var opinions: MutableMap<String, Int> = mutableMapOf()
override var opinions: OpinionsMutable = mutableMapOf()
}

View File

@@ -1,11 +1,12 @@
package fr.dcproject.entity
import fr.dcproject.entity.UserI.Roles
import fr.postgresjson.entity.immutable.*
import io.ktor.auth.Principal
import fr.postgresjson.entity.*
import io.ktor.auth.*
import org.joda.time.DateTime
import java.util.*
@Deprecated("")
class User(
id: UUID = UUID.randomUUID(),
username: String,
@@ -16,6 +17,7 @@ class User(
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()
@Deprecated("")
open class UserBasic(
id: UUID = UUID.randomUUID(),
override var username: String,
@@ -30,12 +32,19 @@ interface UserI : UuidEntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN }
}
@Deprecated("")
interface UserBasicI : UserI {
var username: String
var blockedAt: DateTime?
}
@Deprecated("")
interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt {
var plainPassword: String?
var roles: List<Roles>
}
interface UserForAuthI : UserI {
var roles: List<Roles>
var blockedAt: DateTime?
}

View File

@@ -0,0 +1,17 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityVersioning
import java.util.*
interface VersionableRef {
val versionId: UUID
}
class VersionableRefImp (
override val versionId: UUID
) : VersionableRef
interface Versionable: VersionableRef, EntityVersioning<UUID, Int> {
override val versionId: UUID
override val versionNumber: Int
}

View File

@@ -1,10 +1,10 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.immutable.EntityUpdatedAt
import fr.postgresjson.entity.immutable.EntityUpdatedAtImp
import fr.postgresjson.entity.EntityUpdatedAt
import fr.postgresjson.entity.EntityUpdatedAtImp
open class ViewAggregation(
class ViewAggregation(
val total: Int,
val unique: Int
) : EntityI,

View File

@@ -1,9 +0,0 @@
package fr.dcproject.entity
interface Viewable {
var views: ViewAggregation
}
class ViewableImp : Viewable {
override var views: ViewAggregation = ViewAggregation()
}

View File

@@ -1,9 +1,9 @@
package fr.dcproject.entity
interface Votable {
var votes: VoteAggregation
val votes: VoteAggregation
}
class VotableImp : Votable {
override var votes: VoteAggregation = VoteAggregation()
override val votes: VoteAggregation = VoteAggregation()
}

View File

@@ -1,16 +1,16 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.*
import java.util.*
open class Vote<T : TargetI>(
@Deprecated("")
class Vote<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override var target: T,
override val target: T,
var note: Int,
var anonymous: Boolean = true
) : ExtraI<T, CitizenBasicI>,
UuidEntity(id),
VoteRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp() {
@@ -20,3 +20,26 @@ open class Vote<T : TargetI>(
}
}
}
class VoteForUpdate<T: TargetI, C: CitizenI>(
override val id: UUID = UUID.randomUUID(),
override val note: Int,
override val target: T,
override val createdBy: C
) : VoteRef(id),
VoteForUpdateI<T, C>,
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
interface VoteForUpdateI<T: TargetI, C: CitizenI> : VoteI, AsTarget<T>, EntityCreatedBy<C> {
override val id: UUID
val note: Int
override val target: T
override val createdBy: C
}
open class VoteRef(
override val id: UUID
) : VoteI
interface VoteI : UuidEntityI

View File

@@ -1,8 +1,8 @@
package fr.dcproject.entity
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.mutable.EntityUpdatedAt
import fr.postgresjson.entity.mutable.EntityUpdatedAtImp
import fr.postgresjson.entity.EntityUpdatedAt
import fr.postgresjson.entity.EntityUpdatedAtImp
open class VoteAggregation(
val up: Int,

View File

@@ -2,22 +2,21 @@ package fr.dcproject.entity
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
import fr.postgresjson.entity.*
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.*
class Workgroup(
id: UUID? = null,
name: String,
description: String,
logo: String? = null,
anonymous: Boolean = true,
createdBy: CitizenBasic,
override var members: List<Member<CitizenBasic>> = emptyList()
) : WorkgroupWithAuthI<CitizenBasic>,
WorkgroupSimple<CitizenBasic>(
@Deprecated("")
data class Workgroup <C: CitizenBasicI>(
override val id: UUID = UUID.randomUUID(),
override var name: String,
override var description: String,
override var logo: String? = null,
override var anonymous: Boolean = true,
override val createdBy: C,
override var members: List<Member<C>> = emptyList()
) : WorkgroupWithAuthI<C>,
WorkgroupSimple<C>(
id,
name,
description,
@@ -28,17 +27,26 @@ class Workgroup(
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()
open class WorkgroupSimple<Z : CitizenRef>(
@Deprecated("")
open class WorkgroupSimple<Z : CitizenI>(
id: UUID? = null,
var name: String,
var description: String,
var logo: String? = null,
var anonymous: Boolean = true,
open var name: String,
open var description: String,
open var logo: String? = null,
open var anonymous: Boolean = true,
createdBy: Z
) : WorkgroupRef(id),
EntityCreatedBy<Z> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp()
class WorkgroupCart(
override val id: UUID,
override val name: String
) : WorkgroupCartI
interface WorkgroupCartI : UuidEntityI {
val name: String
}
open class WorkgroupRef(
id: UUID? = null
) : UuidEntity(id ?: UUID.randomUUID()), WorkgroupI

View File

@@ -3,7 +3,7 @@ package fr.dcproject.event
import com.rabbitmq.client.*
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import fr.dcproject.Config
import fr.dcproject.entity.Article
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.FollowSimple
import fr.dcproject.entity.TargetRef
@@ -11,10 +11,8 @@ import fr.dcproject.event.publisher.Publisher
import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.repository.Follow
import fr.postgresjson.serializer.deserialize
import io.ktor.application.ApplicationCall
import io.ktor.application.EventDefinition
import io.ktor.application.application
import io.ktor.util.pipeline.PipelineContext
import io.ktor.application.*
import io.ktor.util.pipeline.*
import io.lettuce.core.api.async.RedisAsyncCommands
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
@@ -26,7 +24,7 @@ import org.slf4j.LoggerFactory
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
class ArticleUpdate(
target: Article
target: ArticleForView
) : EntityEvent(target, "article", "update") {
companion object {
val event = EventDefinition<ArticleUpdate>()

View File

@@ -1,10 +1,9 @@
package fr.dcproject.event
import fr.postgresjson.entity.Serializable
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.UuidEntity
import io.ktor.application.*
import io.ktor.util.AttributeKey
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.*
import kotlinx.coroutines.DisposableHandle
import org.joda.time.DateTime
import kotlin.random.Random.Default.nextInt

View File

@@ -3,11 +3,15 @@ package fr.dcproject.messages
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.entity.*
import fr.postgresjson.entity.immutable.UuidEntityI
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleWithTitleI
import fr.dcproject.entity.CitizenBasicI
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.FollowSimple
import fr.dcproject.entity.TargetRef
import fr.postgresjson.entity.UuidEntityI
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Article as ArticleRepository
class NotificationEmailSender(
private val mailer: Mailer,
@@ -40,7 +44,7 @@ class NotificationEmailSender(
private fun generateHtmlContent(citizen: CitizenBasicI, target: UuidEntityI): String? {
return when (target) {
is Article -> """
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent()
@@ -50,7 +54,7 @@ class NotificationEmailSender(
private fun generateContent(citizen: CitizenBasicI, target: UuidEntityI): String {
return when (target) {
is Article -> """
is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent()

View File

@@ -1,32 +1,28 @@
package fr.dcproject.repository
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.*
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.immutable.UuidEntityI
import fr.postgresjson.entity.UuidEntityI
import fr.postgresjson.repository.RepositoryI
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.entity.Article as ArticleEntity
abstract class Comment<T : TargetI>(override var requester: Requester) : RepositoryI {
abstract fun findById(id: UUID): CommentEntity<T>?
abstract fun findById(id: UUID): CommentForView<T, CitizenRef>?
abstract fun findByCitizen(
citizen: CitizenEntity,
citizen: CitizenI,
page: Int = 1,
limit: Int = 50
): Paginated<CommentEntity<T>>
): Paginated<CommentForView<T, CitizenRef>>
open fun findByParent(
parent: CommentEntity<T>,
parent: CommentForView<T, CitizenRef>,
page: Int = 1,
limit: Int = 50
): Paginated<CommentEntity<T>> {
): Paginated<CommentForView<T, CitizenRef>> {
return findByParent(parent.id, page, limit)
}
@@ -34,7 +30,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
parentId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<CommentEntity<T>> {
): Paginated<CommentForView<T, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_parent")
.select(
@@ -49,7 +45,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
page: Int = 1,
limit: Int = 50,
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
): Paginated<CommentEntity<T>> {
): Paginated<CommentForView<T, CitizenRef>> {
return findByTarget(target.id, page, limit, sort)
}
@@ -58,7 +54,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
page: Int = 1,
limit: Int = 50,
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
): Paginated<CommentEntity<T>> {
): Paginated<CommentForView<T, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_target")
.select(
@@ -69,7 +65,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
}
}
fun <I : T> comment(comment: CommentEntity<I>) {
fun <I : T, C: CitizenRef> comment(comment: CommentForUpdate<I, C>) {
requester
.getFunction("comment")
.sendQuery(
@@ -78,7 +74,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
)
}
fun <I : T> edit(comment: CommentEntity<I>) {
fun <I : T> edit(comment: CommentForUpdate<I, CitizenRef>) {
requester
.getFunction("edit_comment")
.sendQuery(
@@ -89,17 +85,17 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
}
class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
override fun findById(id: UUID): CommentEntity<TargetRef>? {
override fun findById(id: UUID): CommentForView<TargetRef, CitizenRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentEntity<TargetRef>> {
): Paginated<CommentForView<TargetRef, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
@@ -113,7 +109,7 @@ class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
parentId: UUID,
page: Int,
limit: Int
): Paginated<CommentEntity<TargetRef>> {
): Paginated<CommentForView<TargetRef, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_parent")
.select(
@@ -124,18 +120,18 @@ class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
}
}
class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
override fun findById(id: UUID): CommentEntity<ArticleEntity>? {
class CommentArticle(requester: Requester) : Comment<ArticleForView>(requester) {
override fun findById(id: UUID): CommentForView<ArticleForView, CitizenRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentEntity<ArticleEntity>> {
): Paginated<CommentForView<ArticleForView, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
@@ -151,7 +147,7 @@ class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
page: Int,
limit: Int,
sort: Sort
): Paginated<CommentEntity<ArticleEntity>> = requester
): Paginated<CommentForView<ArticleForView, CitizenRef>> = requester
.getFunction("find_comments_by_target")
.select(
page, limit,
@@ -171,17 +167,17 @@ class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
}
class CommentConstitution(requester: Requester) : Comment<ConstitutionRef>(requester) {
override fun findById(id: UUID): CommentEntity<ConstitutionRef>? {
override fun findById(id: UUID): CommentForView<ConstitutionRef, CitizenRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenEntity,
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentEntity<ConstitutionRef>> {
): Paginated<CommentForView<ConstitutionRef, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
@@ -197,7 +193,7 @@ class CommentConstitution(requester: Requester) : Comment<ConstitutionRef>(reque
page: Int,
limit: Int,
sort: CommentArticle.Sort
): Paginated<CommentEntity<ConstitutionRef>> {
): Paginated<CommentForView<ConstitutionRef, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_target")
.select(

View File

@@ -1,7 +1,7 @@
package fr.dcproject.repository
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.CitizenSimple
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.entity.ConstitutionSimple
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
@@ -34,7 +34,7 @@ class Constitution(override var requester: Requester) : RepositoryI {
)
}
fun upsert(constitution: ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>>): ConstitutionEntity? {
fun upsert(constitution: ConstitutionSimple<CitizenWithUserI, ConstitutionSimple.TitleSimple<ArticleRef>>): ConstitutionEntity? {
return requester
.getFunction("upsert_constitution")
.selectOne("resource" to constitution)

View File

@@ -1,14 +1,15 @@
package fr.dcproject.repository
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.*
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.repository.RepositoryI
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Follow as FollowEntity
@@ -33,7 +34,7 @@ sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Req
)
}
fun follow(follow: FollowEntity<IN>) {
fun follow(follow: FollowForUpdate<IN, *>) {
requester
.getFunction("follow")
.sendQuery(
@@ -43,7 +44,7 @@ sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Req
)
}
fun unfollow(follow: FollowEntity<IN>) {
fun unfollow(follow: FollowForUpdate<IN, *>) {
requester
.getFunction("unfollow")
.sendQuery(
@@ -86,12 +87,12 @@ sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Req
): Paginated<FollowSimple<IN, CitizenRef>>
}
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(requester) {
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleForView>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ArticleEntity>> {
): Paginated<FollowEntity<ArticleForView>> {
return requester.run {
getFunction("find_follows_article_by_citizen")
.select(

View File

@@ -1,9 +1,10 @@
package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.entity.ArticleRef
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.OpinionForUpdate
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
@@ -70,7 +71,7 @@ abstract class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requ
fun updateOpinions(choice: OpinionChoiceRef, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>> =
updateOpinions(listOf(choice), citizen, target)
abstract fun addOpinion(opinion: OpinionEntity<T>): OpinionEntity<T>
abstract fun addOpinion(opinion: OpinionForUpdate<T>): OpinionEntity<T>
/**
* Find opinions of one citizen filtered by target ids
@@ -148,7 +149,7 @@ class OpinionArticle(requester: Requester) : Opinion<ArticleRef>(requester) {
/**
* Add Opinions on Article
*/
override fun addOpinion(opinion: OpinionEntity<ArticleRef>): OpinionArticleEntity {
override fun addOpinion(opinion: OpinionForUpdate<ArticleRef>): OpinionArticleEntity {
return requester
.getFunction("upsert_opinion")
.selectOne("resource" to opinion)!!

View File

@@ -1,9 +1,8 @@
package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.*
import fr.dcproject.entity.Article
import fr.dcproject.entity.Comment
import fr.dcproject.entity.Constitution
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
@@ -13,9 +12,8 @@ import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Vote as VoteEntity
open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {
fun vote(vote: VoteEntity<T>): VoteAggregation {
fun vote(vote: VoteForUpdateI<T, *>, anonymous: Boolean? = null): VoteAggregation {
val author = vote.createdBy
val anonymous = author.voteAnonymous
return requester
.getFunction("vote")
.selectOne(
@@ -62,46 +60,46 @@ open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {
}
}
class VoteArticle(requester: Requester) : Vote<Article>(requester) {
class VoteArticle(requester: Requester) : Vote<ArticleForView>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Article>> =
): Paginated<VoteEntity<ArticleForView>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<Article>>>() {},
object : TypeReference<List<VoteEntity<ArticleForView>>>() {},
page,
limit
)
}
class VoteArticleComment(requester: Requester) : Vote<Comment<Article>>(requester) {
class VoteArticleComment(requester: Requester) : Vote<CommentForView<ArticleForView, CitizenRef>>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Comment<Article>>> =
): Paginated<VoteEntity<CommentForView<ArticleForView, CitizenRef>>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<Comment<Article>>>>() {},
object : TypeReference<List<VoteEntity<CommentForView<ArticleForView, CitizenRef>>>>() {},
page,
limit
)
}
class VoteComment(requester: Requester) : Vote<Comment<TargetRef>>(requester) {
class VoteComment(requester: Requester) : Vote<CommentForView<TargetRef, CitizenRef>>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Comment<TargetRef>>> =
): Paginated<VoteEntity<CommentForView<TargetRef, CitizenRef>>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<Comment<TargetRef>>>>() {},
object : TypeReference<List<VoteEntity<CommentForView<TargetRef, CitizenRef>>>>() {},
page,
limit
)

View File

@@ -13,7 +13,7 @@ import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity
class Workgroup(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): WorkgroupEntity? {
fun findById(id: UUID): WorkgroupEntity<CitizenBasic>? {
val function = requester.getFunction("find_workgroup_by_id")
return function.selectOne("id" to id)
}
@@ -25,7 +25,7 @@ class Workgroup(override var requester: Requester) : RepositoryI {
direction: Direction? = null,
search: String? = null,
filter: Filter = Filter()
): Paginated<WorkgroupEntity> {
): Paginated<WorkgroupEntity<CitizenBasic>> {
return requester
.getFunction("find_workgroups")
.select(
@@ -37,11 +37,11 @@ class Workgroup(override var requester: Requester) : RepositoryI {
)
}
fun upsert(workgroup: WorkgroupSimple<CitizenRef>): WorkgroupEntity = requester
fun <C: CitizenI, W: WorkgroupSimple<C>> upsert(workgroup: W): WorkgroupEntity<CitizenBasic> = requester
.getFunction("upsert_workgroup")
.selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null")
fun delete(workgroup: WorkgroupRef) = requester
fun <W: WorkgroupRef> delete(workgroup: W) = requester
.getFunction("delete_workgroup")
.perform("id" to workgroup.id)

View File

@@ -1,142 +0,0 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.ArticleForUpdate
import fr.dcproject.entity.WorkgroupRef
import fr.dcproject.event.ArticleUpdate
import fr.dcproject.event.raiseEvent
import fr.dcproject.repository.Article.Filter
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
import fr.dcproject.security.voter.ArticleVoter.Action.UPDATE
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
import fr.dcproject.views.ArticleViewManager
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import kotlinx.coroutines.launch
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
@KtorExperimentalLocationsAPI
object ArticlesPaths {
@Location("/articles")
class ArticlesRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null,
val createdBy: String? = null,
val workgroup: String? = null
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@Location("/articles/{article}")
class ArticleRequest(val article: ArticleEntity)
@Location("/articles/{article}/versions")
class ArticleVersionsRequest(
val article: ArticleEntity,
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@Location("/articles")
class PostArticleRequest : KoinComponent {
class Article(
val id: UUID?,
val title: String,
val anonymous: Boolean = true,
val content: String,
val description: String,
val tags: List<String> = emptyList(),
val draft: Boolean = false,
val versionId: UUID?,
val workgroup: WorkgroupRef? = null
)
private val workgroupRepository: WorkgroupRepository by inject()
suspend fun getNewArticle(call: ApplicationCall): ArticleForUpdate = call.receive<Article>().run {
ArticleForUpdate(
id = id ?: UUID.randomUUID(),
title = title,
anonymous = anonymous,
content = content,
description = description,
tags = tags,
draft = draft,
createdBy = call.citizen,
workgroup = if (workgroup != null) workgroupRepository.findById(workgroup.id) else null,
versionId = versionId
)
}
}
}
@KtorExperimentalLocationsAPI
fun Route.article(repo: ArticleRepository, viewManager: ArticleViewManager) {
get<ArticlesPaths.ArticlesRequest> {
val articles = repo.find(
it.page,
it.limit,
it.sort,
it.direction,
it.search,
Filter(createdById = it.createdBy, workgroupId = it.workgroup)
)
assertCanAll(VIEW, articles.result)
call.respond(articles)
}
get<ArticlesPaths.ArticleRequest> {
assertCan(VIEW, it.article)
it.article.views = viewManager.getViewsCount(it.article)
call.respond(it.article)
launch {
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull)
}
}
get<ArticlesPaths.ArticleVersionsRequest> {
assertCan(VIEW, it.article)
repo.findVerionsByVersionsId(it.page, it.limit, it.article.versionId).let {
call.respond(it)
}
}
post<ArticlesPaths.PostArticleRequest> {
it.getNewArticle(call).also { article ->
if(article.isNew) {
assertCan(CREATE, article)
} else {
assertCan(UPDATE, article)
}
val newArticle = repo.upsert(article) ?: error("Article not updated")
call.respond(article)
raiseEvent(ArticleUpdate.event, ArticleUpdate(newArticle))
}
}
}

View File

@@ -1,12 +1,10 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Comment
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.CommentRef
import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.UPDATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.dcproject.security.voter.CommentVoter.Action.*
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import io.ktor.application.*
@@ -67,7 +65,7 @@ fun Route.comment(repo: CommentRepository) {
post<CommentPaths.CreateCommentRequest> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = Comment(
val newComment = CommentForUpdate(
content = call.receive<Content>().content,
createdBy = citizen,
parent = parent
@@ -83,6 +81,7 @@ fun Route.comment(repo: CommentRepository) {
val comment = repo.findById(it.comment.id)!!
assertCan(UPDATE, comment)
comment.content = call.receiveText()
repo.edit(comment)

View File

@@ -1,8 +1,10 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Article
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.repository.CommentArticle.Sort
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
@@ -14,14 +16,13 @@ import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
@KtorExperimentalLocationsAPI
object CommentArticlePaths {
@Location("/articles/{article}/comments")
class ArticleCommentRequest(
val article: Article,
val article: ArticleRef,
page: Int = 1,
limit: Int = 50,
val search: String? = null,
@@ -34,14 +35,14 @@ object CommentArticlePaths {
@Location("/articles/{article}/comments")
class PostArticleCommentRequest(
val article: Article
val article: ArticleForView
) {
class Comment(
val content: String
)
suspend fun getComment(call: ApplicationCall) = call.receive<Comment>().run {
CommentEntity(
CommentForUpdate(
target = article,
createdBy = call.citizen,
content = content

View File

@@ -2,6 +2,7 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
@@ -13,7 +14,6 @@ import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
@KtorExperimentalLocationsAPI
@@ -35,7 +35,7 @@ fun Route.commentConstitution(repo: CommentConstitutionRepository) {
post<CommentConstitutionPaths.ConstitutionCommentRequest> {
val content = call.receiveText()
val comment = CommentEntity(
val comment = CommentForUpdate(
target = it.constitution,
createdBy = citizen,
content = content

View File

@@ -1,14 +1,15 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.ArticleRef
import fr.dcproject.entity.CitizenSimple
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.entity.ConstitutionSimple.TitleSimple
import fr.dcproject.security.voter.ConstitutionVoter.Action.CREATE
import fr.dcproject.security.voter.ConstitutionVoter.Action.VIEW
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.*
import io.ktor.locations.*
@@ -58,18 +59,19 @@ object ConstitutionPaths {
var rank: Int? = null,
var articles: MutableList<ArticleRef> = mutableListOf()
) : UuidEntity(id) {
fun create(): ConstitutionSimple.TitleSimple<ArticleRef> =
ConstitutionSimple.TitleSimple(
fun create(): TitleSimple<ArticleRef> =
TitleSimple(
id, name, rank, articles
)
}
fun List<Title>.create(): MutableList<ConstitutionSimple.TitleSimple<ArticleRef>> =
fun List<Title>.create(): MutableList<TitleSimple<ArticleRef>> =
map { it.create() }.toMutableList()
}
suspend fun getNewConstitution(call: ApplicationCall): ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>> = call.receive<Constitution>().run {
ConstitutionSimple(
suspend fun getNewConstitution(call: ApplicationCall): ConstitutionSimple<CitizenWithUserI, TitleSimple<ArticleRef>> = call.receive<Constitution>().run {
ConstitutionSimple<CitizenWithUserI, TitleSimple<ArticleRef>>(
id = UUID.randomUUID(),
title = title,
titles = titles.create(),
createdBy = call.citizen,

View File

@@ -1,11 +1,10 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.ArticleRef
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.Citizen
import fr.dcproject.security.voter.FollowVoter.Action.CREATE
import fr.dcproject.security.voter.FollowVoter.Action.DELETE
import fr.dcproject.security.voter.FollowVoter.Action.VIEW
import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.security.voter.FollowVoter.Action.*
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import io.ktor.application.*
@@ -13,7 +12,6 @@ import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
@KtorExperimentalLocationsAPI
@@ -28,14 +26,14 @@ object FollowArticlePaths {
@KtorExperimentalLocationsAPI
fun Route.followArticle(repo: FollowArticleRepository) {
post<FollowArticlePaths.ArticleFollowRequest> {
val follow = FollowEntity(target = it.article, createdBy = this.citizen)
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
assertCan(CREATE, follow)
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
delete<FollowArticlePaths.ArticleFollowRequest> {
val follow = FollowEntity(target = it.article, createdBy = this.citizen)
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
assertCan(DELETE, follow)
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)

View File

@@ -3,9 +3,8 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.security.voter.FollowVoter.Action.CREATE
import fr.dcproject.security.voter.FollowVoter.Action.DELETE
import fr.dcproject.security.voter.FollowVoter.Action.VIEW
import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.security.voter.FollowVoter.Action.*
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import io.ktor.application.*
@@ -13,7 +12,6 @@ import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
@KtorExperimentalLocationsAPI
@@ -28,14 +26,14 @@ object FollowConstitutionPaths {
@KtorExperimentalLocationsAPI
fun Route.followConstitution(repo: FollowConstitutionRepository) {
post<FollowConstitutionPaths.ConstitutionFollowRequest> {
val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
assertCan(CREATE, follow)
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
delete<FollowConstitutionPaths.ConstitutionFollowRequest> {
val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
assertCan(DELETE, follow)
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)

View File

@@ -1,6 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE
@@ -18,7 +19,6 @@ import io.ktor.util.*
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
@@ -39,7 +39,7 @@ object OpinionArticlePaths {
*/
@Location("/articles/{article}/opinions")
@KtorExperimentalAPI
class ArticleOpinion(val article: ArticleEntity) {
class ArticleOpinion(val article: ArticleForView) {
class Body(ids: List<String>) {
val ids = ids.map { OpinionChoiceRef(it.toUUID()) }
}

View File

@@ -1,7 +1,9 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.repository.CommentGeneric
import fr.dcproject.repository.VoteComment
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
@@ -18,14 +20,12 @@ import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Vote as VoteEntity
import fr.dcproject.repository.VoteArticle as VoteArticleRepository
@KtorExperimentalLocationsAPI
object VoteArticlePaths {
@Location("/articles/{article}/vote")
class ArticleVoteRequest(val article: ArticleEntity) {
class ArticleVoteRequest(val article: ArticleForView) {
data class Content(var note: Int)
}
@@ -52,7 +52,7 @@ object VoteArticlePaths {
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentGeneric) {
put<ArticleVoteRequest> {
val content = call.receive<ArticleVoteRequest.Content>()
val vote = VoteEntity(
val vote = VoteForUpdate(
target = it.article,
note = content.note,
createdBy = this.citizen
@@ -65,7 +65,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
put<CommentVoteRequest> {
val comment = commentRepo.findById(it.comment)!!
val content = call.receive<CommentVoteRequest.Content>()
val vote = VoteEntity(
val vote = VoteForUpdate(
target = comment,
note = content.note,
createdBy = this.citizen

View File

@@ -2,19 +2,17 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
import fr.ktorVoter.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.put
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Vote as VoteEntity
import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
@KtorExperimentalLocationsAPI
@@ -32,7 +30,7 @@ object VoteConstitutionPaths {
fun Route.voteConstitution(repo: VoteConstitutionRepository) {
put<VoteConstitutionPaths.ConstitutionVoteRequest> {
val content = call.receive<Content>()
val vote = VoteEntity(
val vote = VoteForUpdate(
target = it.constitution,
note = content.note,
createdBy = this.citizen

View File

@@ -6,9 +6,9 @@ import fr.dcproject.entity.WorkgroupSimple
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
import fr.dcproject.repository.Workgroup.Filter
import fr.dcproject.security.voter.WorkgroupVoter.Action.CREATE
import fr.dcproject.routes.WorkgroupsPaths.PutWorkgroupRequest.Input
import fr.dcproject.security.voter.WorkgroupVoter.Action.*
import fr.dcproject.security.voter.WorkgroupVoter.Action.UPDATE
import fr.dcproject.security.voter.WorkgroupVoter.Action.VIEW
import fr.dcproject.utils.toUUID
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
@@ -19,9 +19,10 @@ import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity
import fr.dcproject.repository.Workgroup as WorkgroupRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepo
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.ADD as ADD_MEMBERS
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.REMOVE as REMOVE_MEMBERS
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.UPDATE as UPDATE_MEMBERS
@@ -43,8 +44,11 @@ object WorkgroupsPaths {
val members: List<UUID>? = members?.toUUID()
}
@Location("/workgroups/{workgroup}")
class WorkgroupRequest(val workgroup: WorkgroupEntity)
@Location("/workgroups/{workgroupId}")
class WorkgroupRequest(private val workgroupId: UUID) : KoinComponent {
val repo: WorkgroupRepo by inject()
val workgroup = repo.findById(workgroupId) ?: TODO()
}
@Location("/workgroups")
open class PostWorkgroupRequest {
@@ -68,31 +72,30 @@ object WorkgroupsPaths {
}
}
@Location("/workgroups/{workgroup}")
class PutWorkgroupRequest(val workgroup: WorkgroupEntity) {
class Body(
@Location("/workgroups/{workgroupId}")
class PutWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
class Input(
val name: String?,
val description: String?,
val logo: String?,
val anonymous: Boolean?
)
suspend fun updateWorkgroup(call: ApplicationCall): Unit = call.receive<Body>().run {
name?.let { workgroup.name = it }
description?.let { workgroup.description = it }
logo?.let { workgroup.logo = it }
anonymous?.let { workgroup.anonymous = it }
}
}
@Location("/workgroups/{workgroup}")
class DeleteWorkgroupRequest(val workgroup: WorkgroupEntity)
@Location("/workgroups/{workgroupId}")
class DeleteWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
val repo: WorkgroupRepo by inject()
val workgroup = repo.findById(workgroupId)
}
}
@KtorExperimentalLocationsAPI
object WorkgroupsMembersPaths {
@Location("/workgroups/{workgroup}/members")
class WorkgroupsMembersRequest(val workgroup: WorkgroupEntity) {
@Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
val repo: WorkgroupRepo by inject()
val workgroup = repo.findById(workgroupId)
class Body : MutableList<Body.Item> by mutableListOf() {
class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) {
val roles: List<Role> = roles.map {
@@ -111,7 +114,7 @@ object WorkgroupsMembersPaths {
}
@KtorExperimentalLocationsAPI
fun Route.workgroup(repo: WorkgroupRepository) {
fun Route.workgroup(repo: WorkgroupRepo) {
get<WorkgroupsPaths.WorkgroupsRequest> {
val workgroups =
repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy, members = it.members))
@@ -136,50 +139,74 @@ fun Route.workgroup(repo: WorkgroupRepository) {
}
put<WorkgroupsPaths.PutWorkgroupRequest> {
it.updateWorkgroup(call).let { workgroup ->
assertCan(UPDATE, workgroup)
repo.upsert(workgroup as WorkgroupSimple<CitizenRef>)
}.let {
call.respond(HttpStatusCode.OK, it)
}
repo.findById(it.workgroupId)?.let { old ->
call.receive<Input>().run {
old.copy(
name = name ?: old.name,
description = description ?: old.description,
logo = logo ?: old.logo,
anonymous = anonymous ?: old.anonymous
).let { workgroup ->
assertCan(UPDATE, workgroup)
repo.upsert(workgroup)
call.respond(HttpStatusCode.OK, it)
}
}
} ?: call.respond(HttpStatusCode.NotFound)
}
delete<WorkgroupsPaths.DeleteWorkgroupRequest> {
assertCan(UPDATE, it.workgroup)
repo.delete(it.workgroup)
call.respond(HttpStatusCode.NoContent, it)
if (it.workgroup != null) {
assertCan(DELETE, it.workgroup)
repo.delete(it.workgroup)
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
/* Add members to workgroup */
post<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
it.getMembers(call)
.let { members ->
assertCan(ADD_MEMBERS, it.workgroup)
repo.addMembers(it.workgroup, members)
}.let { members ->
call.respond(HttpStatusCode.Created, members)
}
if (it.workgroup != null) {
it.getMembers(call)
.let { members ->
assertCan(ADD_MEMBERS, it.workgroup)
repo.addMembers(it.workgroup, members)
}.let { members ->
call.respond(HttpStatusCode.Created, members)
}
} else {
call.respond(HttpStatusCode.NotFound)
}
}
/* Delete members of workgroup */
delete<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
it.getMembers(call)
.let { members ->
assertCan(REMOVE_MEMBERS, it.workgroup)
repo.removeMembers(it.workgroup, members)
}.let { members ->
call.respond(HttpStatusCode.OK, members)
}
if (it.workgroup != null) {
it.getMembers(call)
.let { members ->
assertCan(REMOVE_MEMBERS, it.workgroup)
repo.removeMembers(it.workgroup, members)
}.let { members ->
call.respond(HttpStatusCode.OK, members)
}
} else {
call.respond(HttpStatusCode.NotFound)
}
}
/* Update members of workgroup */
put<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
it.getMembers(call)
.let { members ->
assertCan(UPDATE_MEMBERS, it.workgroup)
repo.updateMembers(it.workgroup, members)
}.let { members ->
call.respond(HttpStatusCode.OK, members)
}
if (it.workgroup != null) {
it.getMembers(call)
.let { members ->
assertCan(UPDATE_MEMBERS, it.workgroup)
repo.updateMembers(it.workgroup, members)
}.let { members ->
call.respond(HttpStatusCode.OK, members)
}
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}

View File

@@ -3,4 +3,4 @@ package fr.dcproject.utils
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
fun DateTime.toIso() = ISODateTimeFormat.dateTime().print(this)
fun DateTime.toIso(): String = ISODateTimeFormat.dateTime().print(this)

View File

@@ -6,5 +6,5 @@ import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
internal class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
override fun getValue(thisRef: R, property: KProperty<*>) = LoggerFactory.getLogger(thisRef.javaClass.packageName)
override fun getValue(thisRef: R, property: KProperty<*>): Logger = LoggerFactory.getLogger(thisRef.javaClass.packageName)
}

View File

@@ -1,7 +1,7 @@
package fr.dcproject.utils
fun String.readResource(callbak: (String) -> Unit = {}): String {
val content = callbak::class.java.getResource(this).readText()
callbak(content)
fun String.readResource(callback: (String) -> Unit = {}): String {
val content = callback::class.java.getResource(this).readText()
callback(content)
return content
}

View File

@@ -1,11 +1,18 @@
package fr.dcproject.views
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.ViewAggregation
import org.elasticsearch.client.Response
import org.joda.time.DateTime
interface ViewManager <T> {
fun addView(ip: String, entity: T, citizen: CitizenRef? = null, dateTime: DateTime = DateTime.now()): Response?
/**
* Add view to one entity
*/
fun addView(ip: String, entity: T, citizen: CitizenI? = null, dateTime: DateTime = DateTime.now()): Response?
/**
* Get Views aggregations
*/
fun getViewsCount(entity: T): ViewAggregation
}

View File

@@ -1,116 +0,0 @@
package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.ArticleAuthI
import fr.dcproject.entity.ArticleForUpdateI
import fr.dcproject.entity.ArticleI
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.UserI
import fr.dcproject.repository.Article as ArticleRepo
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Vote.Companion.toVote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import org.koin.core.KoinComponent
import org.koin.core.inject
import fr.dcproject.entity.Comment as CommentEntity
import fr.dcproject.entity.Vote as VoteEntity
class ArticleVoter(private val articleRepo: ArticleRepo) : Voter<ApplicationCall> {
enum class Action : ActionI {
CREATE,
UPDATE,
VIEW,
DELETE
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
if (!((action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
&& (subject is ArticleI? || subject is VoteEntity<*> || subject is CommentEntity<*>))
) return Vote.ABSTAIN
val user = context.user
if (action == Action.CREATE && user is UserI) return Vote.GRANTED
if (action == Action.VIEW) return view(subject, user)
if (action == Action.DELETE) return delete(subject, user)
if (action == Action.UPDATE) return update(subject, context.citizenOrNull)
if (action is CommentVoter.Action) return voteForComment(action, subject)
if (action is VoteVoter.Action) return voteForVote(action, subject)
if (action is Action) return Vote.DENIED
return Vote.ABSTAIN
}
private fun view(subject: Any?, user: UserI?): Vote {
if (subject is ArticleAuthI<*>) {
return if (subject.isDeleted()) Vote.DENIED
else if (subject.draft && (user == null || subject.createdBy.user.id != user.id)) Vote.DENIED
else Vote.GRANTED
}
return Vote.DENIED
}
private fun delete(subject: Any?, user: UserI?): Vote {
if (subject is ArticleAuthI<*>) {
if (user is UserI && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
}
}
return Vote.DENIED
}
private fun update(subject: Any?, citizen: CitizenEntity?): Vote {
/* The new Article must by created by the same citizen of the connected citizen */
if (subject is ArticleForUpdateI && citizen is CitizenI && subject.createdBy.id == citizen.id) {
/* The creator must be the same of the creator of preview version of article */
return toVote {
articleRepo
.findVerionsByVersionsId(1, 1, subject.versionId)
.result.first()
.createdBy.id == citizen.id
}
}
return Vote.DENIED
}
private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote {
if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
val target = subject.target
if (target is ArticleAuthI<*>) {
if (target.isDeleted()) {
return Vote.DENIED
}
} else if (target is ArticleI) {
return Vote.DENIED
}
}
return Vote.ABSTAIN
}
private fun voteForComment(action: CommentVoter.Action, subject: Any?): Vote {
if (subject is CommentEntity<*>) {
val target = subject.target
if (target is ArticleAuthI<*>) {
if (target.isDeleted()) {
return Vote.DENIED
}
} else if (target is ArticleI) {
return Vote.DENIED
}
if (action == CommentVoter.Action.CREATE) {
return Vote.GRANTED
}
if (action == CommentVoter.Action.VIEW) {
return Vote.GRANTED
}
} else {
return Vote.DENIED
}
return Vote.ABSTAIN
}
}

View File

@@ -1,13 +1,13 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.CitizenBasicI
import fr.dcproject.entity.UserI
import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import io.ktor.locations.KtorExperimentalLocationsAPI
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
import io.ktor.locations.*
@KtorExperimentalLocationsAPI
class CitizenVoter : Voter<ApplicationCall> {
@@ -19,45 +19,43 @@ class CitizenVoter : Voter<ApplicationCall> {
CHANGE_PASSWORD
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action)
&& (subject is CitizenBasicI?))) return Vote.ABSTAIN
&& (subject is CitizenBasicI?))) return abstain()
val user = context.user
if (action == Action.CREATE && user != null) {
return Vote.GRANTED
return granted()
}
if (action == Action.VIEW) {
if (user == null) return Vote.DENIED
if (user == null) return denied("You must be connected to view citizen", "citizen.view.connected")
if (subject is CitizenBasicI) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted")
else granted()
}
return Vote.DENIED
throw NoRuleDefinedException(action)
}
if (action == Action.DELETE) {
return Vote.DENIED
return denied("You can never deleted a citizen", "citizen.delete.never")
}
if (action == Action.UPDATE &&
user is UserI &&
subject is CitizenBasicI &&
subject.user.id == user.id
) {
return Vote.GRANTED
if (action == Action.UPDATE) {
if (user == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected")
if (subject !is CitizenWithUserI) throw NoSubjectDefinedException(action)
return if (subject.user.id == user.id) granted() else denied("You can only update your citizen", "citizen.update.notYours")
}
if (action == Action.CHANGE_PASSWORD && user != null && subject is CitizenBasicI) {
val userToChange = subject.user
return if (user.id == userToChange.id) {
Vote.GRANTED
granted()
} else {
Vote.DENIED
denied("You can only change your password", "citizen.password.notYours")
}
}
return Vote.DENIED
throw NoRuleDefinedException(action)
}
}

View File

@@ -1,11 +1,14 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Comment
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.CommentForView
import fr.dcproject.entity.CommentI
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
class CommentVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
@@ -15,38 +18,47 @@ class CommentVoter : Voter<ApplicationCall> {
DELETE
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
if (!(action is Action && subject is Comment<*>?)) return Vote.ABSTAIN
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!(action is Action && subject is CommentI?)) return abstain()
val user = context.user
val citizen = context.citizenOrNull
if (subject == null) {
return Vote.DENIED
throw NoSubjectDefinedException(action)
}
if (action == Action.CREATE) {
if (user == null) {
return Vote.DENIED
return when {
citizen == null -> denied("You must be connected to create user", "comment.create.notConnected")
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
subject.createdBy.id != citizen.id -> denied("You cannot create a comment with other user than yours", "comment.create.wrongUser")
subject.parent?.isDeleted() ?: false -> denied("You cannot create a comment on deleted parent", "comment.create.deletedParent")
subject.target.let { it is EntityDeletedAt && it.isDeleted() } -> denied("You cannot create a comment on deleted target", "comment.create.deletedTarget")
else -> granted()
}
if (subject.createdBy.user.id != user.id) {
return Vote.DENIED
}
return Vote.GRANTED
}
if (action == Action.VIEW) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
return when {
subject !is CommentForView<*, *> -> throw NoSubjectDefinedException(action)
subject.isDeleted() -> denied("Your cannot view a deleted comment", "comment.view.deleted")
else -> granted()
}
}
if (action == Action.UPDATE && user != null && user.id == subject.createdBy.user.id) {
return Vote.GRANTED
if (action == Action.UPDATE) {
if (citizen == null) return denied("You must be connected to update comment", "comment.update.notConnected")
return when {
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
citizen.id == subject.createdBy.id -> granted()
else -> denied("You cannot update another user of yours", "comment.update.notYours")
}
}
if (action == Action.DELETE) {
return Vote.DENIED
return denied("A comment can never be deleted", "comment.deleted.never")
}
return Vote.DENIED
throw NoRuleDefinedException(action)
}
}

View File

@@ -1,13 +1,13 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Comment
import fr.dcproject.entity.CommentForView
import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
import fr.dcproject.entity.Vote as VoteEntity
class ConstitutionVoter : Voter<ApplicationCall> {
@@ -18,63 +18,63 @@ class ConstitutionVoter : Voter<ApplicationCall> {
DELETE
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if(!((action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
&& (subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is Comment<*>))) return Vote.ABSTAIN
&& (subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is CommentForView<*, *>))) return abstain()
val user = context.user
if (action == Action.CREATE && user != null) {
return Vote.GRANTED
return granted()
}
if (action == Action.VIEW) {
if (subject is ConstitutionSimple<*, *>) {
return if (subject.isDeleted()) Vote.DENIED
else Vote.GRANTED
return if (subject.isDeleted()) denied("You cannot view a deleted constitution", "constitution.view.deleted")
else granted()
}
return Vote.DENIED
throw NoSubjectDefinedException(action as ActionI)
}
if (action == Action.DELETE && user is UserI && subject is ConstitutionSimple<*, *> && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
return granted()
}
if (action == Action.UPDATE && user is UserI && subject is ConstitutionSimple<*, *> && subject.createdBy.user.id == user.id) {
return Vote.GRANTED
return granted()
}
if (action is CommentVoter.Action) return voteForComment(action)
if (action is VoteVoter.Action) return voteForVote(action, subject)
if (action is Action) {
return Vote.DENIED
throw NoRuleDefinedException(action)
}
return Vote.ABSTAIN
return abstain()
}
private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote {
private fun voteForVote(action: VoteVoter.Action, subject: Any?): VoterResponseI {
if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
val target = subject.target
if (target !is ConstitutionSimple<*, *>) {
return Vote.ABSTAIN
return abstain()
}
if (target.isDeleted()) {
return Vote.DENIED
return denied("You cannot vote a deleted constitution", "constitution.vote.deleted")
}
}
return Vote.ABSTAIN
return abstain()
}
private fun voteForComment(action: CommentVoter.Action): Vote {
private fun voteForComment(action: CommentVoter.Action): VoterResponseI {
if (action == CommentVoter.Action.CREATE) {
return Vote.GRANTED
return granted()
}
if (action == CommentVoter.Action.VIEW) {
return Vote.GRANTED
return granted()
}
return Vote.ABSTAIN
return abstain()
}
}

View File

@@ -1,12 +1,12 @@
package fr.dcproject.security.voter
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.FollowI
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.entity.User as UserEntity
class FollowVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
@@ -15,33 +15,33 @@ class FollowVoter : Voter<ApplicationCall> {
VIEW
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
if (!((action is Action)
&& (subject is FollowEntity<*>?))) return Vote.ABSTAIN
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (action !is Action) return abstain()
if (subject !is FollowI) throw NoSubjectDefinedException(action)
val user = context.user
val citizen = context.citizenOrNull
if (action == Action.CREATE) {
return if (user != null) Vote.GRANTED
else Vote.DENIED
return if (citizen == null) denied("You must be connected to follow", "follow.create.notConnected")
else granted()
}
if (action == Action.DELETE) {
return if (user != null) Vote.GRANTED
else Vote.DENIED
return if (citizen == null) denied("You must be connected to unfollow", "follow.delete.notConnected")
else granted()
}
if (action == Action.VIEW) {
if (subject is FollowEntity<*>) {
return voteView(user, subject)
return voteView(citizen, subject)
}
return Vote.DENIED
throw NoSubjectDefinedException(action)
}
return Vote.ABSTAIN
return abstain()
}
private fun voteView(user: UserEntity?, subject: FollowEntity<*>): Vote {
return if ((user != null && subject.createdBy.user.id == user.id) || !subject.createdBy.followAnonymous) Vote.GRANTED
else Vote.DENIED
private fun voteView(citizen: CitizenI?, subject: FollowEntity<*>): VoterResponseI {
return if ((citizen != null && subject.createdBy.id == citizen.id) || !subject.createdBy.followAnonymous) granted()
else denied("You cannot view an anonymous follow", "follow.view.anonymous")
}
}

View File

@@ -0,0 +1,6 @@
package fr.dcproject.voter
import fr.ktorVoter.ActionI
import fr.ktorVoter.VoterException
class NoRuleDefinedException(action: ActionI) : VoterException("""No rule for action "$action" is defined""")

View File

@@ -0,0 +1,6 @@
package fr.dcproject.voter
import fr.ktorVoter.ActionI
import fr.ktorVoter.VoterException
class NoSubjectDefinedException(action: ActionI) : VoterException("""No subject for action "$action" is defined""")

View File

@@ -1,27 +1,26 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.OpinionChoice
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
class OpinionChoiceVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
VIEW
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action)
&& (subject is OpinionChoice?))) return Vote.ABSTAIN
&& (subject is OpinionChoice?))) return abstain()
if (action == Action.VIEW) {
if (subject is OpinionChoice) {
return Vote.GRANTED
return granted()
}
return Vote.DENIED
throw NoSubjectDefinedException(action)
}
return Vote.ABSTAIN
return abstain()
}
}

View File

@@ -1,14 +1,13 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.Article
import fr.dcproject.entity.ArticleAuthI
import fr.dcproject.component.article.ArticleAuthI
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.Opinion
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Vote.Companion.toVote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
class OpinionVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
@@ -17,32 +16,33 @@ class OpinionVoter : Voter<ApplicationCall> {
DELETE
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action)
&& (subject is Opinion<*>? || subject is ArticleAuthI<*>))) return Vote.ABSTAIN
&& (subject is Opinion<*>? || subject is ArticleAuthI<*>))) return abstain()
val user = context.user
if (action == Action.CREATE) {
return toVote {
user != null && (
(subject is ArticleAuthI<*> && !subject.isDeleted()) ||
(subject is Opinion<*> && subject.createdBy.user.id == user.id)
)
}
if (user == null) return denied("You must be connected to make an opinion", "opinion.create.notConnected")
if (subject is ArticleAuthI<*> && !subject.isDeleted()) return granted()
if (subject is Opinion<*> && subject.createdBy.user.id == user.id) return granted()
throw NoSubjectDefinedException(action)
}
if (action == Action.VIEW) {
return toVote { subject is Opinion<*> || subject is Article }
return if (subject is Opinion<*> || subject is ArticleForView) granted() else throw NoSubjectDefinedException(action)
}
if (action == Action.DELETE) {
return toVote {
subject is Opinion<*> &&
user != null &&
subject.createdBy.user.id == user.id
}
if (user == null) return denied("You must be connected to delete opinion", "opinion.delete.notConnected")
if (subject !is Opinion<*>) throw NoSubjectDefinedException(action)
return if (subject.createdBy.user.id == user.id) granted() else denied("You can only delete your opinions", "opinion.delete.notYours")
}
return Vote.ABSTAIN
if (action is Action) {
throw NoRuleDefinedException(action)
}
return abstain()
}
}

View File

@@ -1,10 +1,12 @@
package fr.dcproject.security.voter
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import io.ktor.application.ApplicationCall
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.VoteForUpdateI
import fr.dcproject.entity.VoteI
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
import fr.dcproject.entity.Vote as VoteEntity
class VoteVoter : Voter<ApplicationCall> {
@@ -13,26 +15,36 @@ class VoteVoter : Voter<ApplicationCall> {
VIEW
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
if (!(action is Action && subject is VoteEntity<*>?)) return Vote.ABSTAIN
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
if (!(action is Action && subject is VoteI)) return abstain()
val user = context.user ?: return Vote.DENIED
val citizen = context.citizenOrNull ?: return denied("You must be connected for vote", "vote.connected")
if (action == Action.CREATE) {
return Vote.GRANTED
if (subject !is VoteForUpdateI<*, *>) throw NoSubjectDefinedException(action)
subject.target.let {
if (it is EntityDeletedAt) {
if (it.isDeleted()) return denied("You cannot vote on deleted target", "vote.create.isDeleted")
} else {
throw NoSubjectDefinedException(action)
}
}
return granted()
}
if (action == Action.VIEW) {
if (subject is VoteEntity<*>) {
return if (subject.createdBy.user.id != user.id) {
Vote.DENIED
return if (subject.createdBy.id != citizen.id) {
denied("You can view only your votes", "vote.view")
} else {
Vote.GRANTED
granted()
}
} else {
throw NoSubjectDefinedException(action)
}
return Vote.DENIED
}
return Vote.ABSTAIN
return abstain()
}
}

View File

@@ -0,0 +1,87 @@
package fr.dcproject.voter
/** Responses of voters */
enum class Vote {
GRANTED,
DENIED;
/** Helper to convert true/false to GRANTED/DENIED */
companion object {
fun toVote(lambda: () -> Boolean): Vote = when (lambda()) {
true -> GRANTED
false -> DENIED
}
}
fun toBoolean(): Boolean = when (this) {
GRANTED -> true
DENIED -> false
}
}
abstract class Voter {
protected fun granted(message: String? = null, code: String? = null): GrantedResponse = GrantedResponse(this, message, code)
protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code)
private fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: granted()
protected fun <S: List<T>, T> canAll(items: S, action: (T) -> VoterResponse): VoterResponse = items
.map { action(it) }
.getOneResponse()
}
fun <T: Voter> T.assert(action: T.() -> VoterResponse) {
action().assert()
}
fun VoterResponses.getOneResponse(): VoterResponse = this.firstOrNull { it.vote == Vote.DENIED } ?: GrantedResponse(first().voter)
fun VoterResponses.assert() = this.getOneResponse().assert()
class VoterDeniedException(private val voterResponses: VoterResponses) : Throwable(voterResponses.first().message) {
constructor(voterResponse: VoterResponse) : this(listOf(voterResponse))
fun first(): VoterResponse = voterResponses.first()
fun hasErrorCode(code: String): Boolean = voterResponses
.filter { it.vote == Vote.DENIED }
.any { it.code == code }
fun getErrorCode(code: String): VoterResponse? = voterResponses
.firstOrNull { it.vote == Vote.DENIED && it.code == code }
fun getMessages(): List<String> = voterResponses
.mapNotNull { it.message }
fun getFirstMessage(): String? = voterResponses
.first()
.message
}
sealed class VoterResponse (
val vote: Vote,
val voter: Voter,
val message: String?,
val code: String?
) {
fun toBoolean(): Boolean = vote.toBoolean()
fun assert() {
if (this.vote == Vote.DENIED) {
throw VoterDeniedException(this)
}
}
}
class GrantedResponse(
voter: Voter,
message: String? = null,
code: String? = null
) : VoterResponse(Vote.GRANTED, voter, message, code)
class DeniedResponse(
voter: Voter,
message: String,
code: String
) : VoterResponse(Vote.DENIED, voter, message, code)
typealias VoterResponses = List<VoterResponse>

View File

@@ -1,13 +1,14 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.*
import fr.dcproject.entity.UserI
import fr.dcproject.entity.WorkgroupI
import fr.dcproject.entity.WorkgroupWithAuthI
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote
import fr.ktorVoter.Voter
import fr.ktorVoter.VoterException
import io.ktor.application.ApplicationCall
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
class WorkgroupVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
@@ -24,67 +25,72 @@ class WorkgroupVoter : Voter<ApplicationCall> {
REMOVE,
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
if (!((action is Action || action is ActionMembers)
&& (subject is WorkgroupI? || (subject is List<*> && subject.first() is WorkgroupI)))) return Vote.ABSTAIN
&& (subject is WorkgroupI? || (subject is List<*> && subject.first() is WorkgroupI)))) return abstain()
val user = context.user
if (subject is WorkgroupI && action == Action.CREATE && user is UserI) {
return Vote.GRANTED
if (action == Action.CREATE) {
if (user == null) return denied("You must be connected to delete workgroup", "workgroup.delete.notConnected")
if (subject is WorkgroupI) {
return granted()
}
}
if (action == Action.VIEW) {
if (subject is WorkgroupWithAuthI<*>) {
return if (subject.isDeleted()) Vote.DENIED
else if (!subject.anonymous) Vote.GRANTED
else if (subject.anonymous && user != null && subject.isMember(user)) Vote.GRANTED
else Vote.DENIED
return if (subject.isDeleted()) denied("You cannot view a deleted workgroup", "workgroup.view.deleted")
else if (!subject.anonymous) granted()
else if (subject.anonymous && user != null && subject.isMember(user)) granted()
else denied("You cannot view anonymous workgroup", "workgroup.view.anonymous")
}
return Vote.DENIED
throw NoSubjectDefinedException(action as ActionI)
}
if (subject is WorkgroupWithAuthI<*>) {
if (action == Action.DELETE && user is UserI && subject.hasRole(Role.MASTER, user)) {
return Vote.GRANTED
if (subject is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
if (action == Action.DELETE) {
if (user == null) return denied("You must be connected to delete workgroup", "workgroup.delete.notConnected")
return if (subject.hasRole(Role.MASTER, user)) granted()
else denied("You must hase role MASTER to delete workgroup", "workgroup.delete.role")
}
if (action == Action.UPDATE) {
if (user == null) return denied("You must be connected to delete workgroup", "workgroup.delete.notConnected")
return if (subject.hasRole(Role.MASTER, user)) granted()
else denied("You must hase role MASTER to delete workgroup", "workgroup.delete.role")
}
if (action == Action.UPDATE && user is UserI && subject.hasRole(Role.MASTER, user)) {
return Vote.GRANTED
}
return Vote.DENIED
throw NoRuleDefinedException(action as ActionI)
} else if (subject !is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
throw object :
VoterException("Unable to define if your are granted, the subject must implement 'WorkgroupWithAuthI'") {}
throw NoSubjectDefinedException(action as ActionI)
}
if (action == ActionMembers.ADD) {
// TODO create ROLES
return Vote.toVote {
user is UserI &&
subject is WorkgroupWithAuthI<*> &&
subject.hasRole(Role.MASTER, user)
}
if (user !is UserI) return denied("You must be connected to add member to the workgroup", "workgroup.addMember.notConnected")
if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for add member to workgroup", "workgroup.addMember.role")
}
if (action == ActionMembers.UPDATE) {
// TODO create ROLES
return Vote.toVote {
user is UserI &&
subject is WorkgroupWithAuthI<*> &&
subject.hasRole(Role.MASTER, user)
}
if (user !is UserI) return denied("You must be connected to update member of the workgroup", "workgroup.updateMember.notConnected")
if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for update members of workgroup", "workgroup.updateMember.role")
}
if (action == ActionMembers.REMOVE) {
// TODO create ROLES
return Vote.toVote {
user is UserI &&
subject is WorkgroupWithAuthI<*> &&
subject.hasRole(Role.MASTER, user)
}
if (user !is UserI) return denied("You must be connected to remove member of the workgroup", "workgroup.removeMember.notConnected")
if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for remove members of workgroup", "workgroup.removeMember.role")
}
return Vote.ABSTAIN
if (action is Action) {
throw NoRuleDefinedException(action)
}
return abstain()
}
}

View File

@@ -1342,8 +1342,6 @@ components:
properties:
user:
$ref: '#/components/schemas/UserResponse'
workgroups:
$ref: '#/components/schemas/WorkgroupSimple'
CitizenBase:
type: object
properties:
@@ -1375,8 +1373,6 @@ components:
properties:
user:
$ref: '#/components/schemas/UserRequest'
workgroups:
$ref: '#/components/schemas/UuidEntity'
RegisterRequest:
$ref: '#/components/schemas/CitizenRequest'

View File

@@ -6,7 +6,16 @@ begin
select to_json(t)
from (
select
a.*,
a.id,
a.version_number,
a.version_id,
a.title,
a.anonymous,
a.content,
a.description,
a.tags,
a.draft,
a.last_version,
find_citizen_by_id_with_user(a.created_by_id) as created_by,
find_workgroup_by_id(a.workgroup_id) as workgroup,
count_vote(a.id) as votes,

View File

@@ -21,11 +21,13 @@ begin
into resource, total
from (
select
a.*,
a.id,
a.title,
a.deleted_at,
a.draft,
find_citizen_by_id_with_user(a.created_by_id) as created_by,
find_workgroup_by_id(a.workgroup_id) as workgroup,
count_vote(a.id) as votes,
count_opinion(a.id) as opinions,
zdb.score(a.ctid) _score
from article as a
left join vote_cache ca using (id)

View File

@@ -54,10 +54,10 @@ begin
insert into article_relations (source_id, target_id, created_by_id)
select
(resource->>'id')::uuid,
(rel->>'id')::uuid,
id,
(resource#>>'{created_by, id}')::uuid
from json_populate_recordset(null::article, resource->>'relations');
(rel#>>'{created_by, id}')::uuid
from json_populate_recordset(null::article, resource->>'relations') rel;
end if;
select find_article_by_id(new_id) into resource;

View File

@@ -8,7 +8,6 @@ begin
select
z.*
from citizen as z
left join citizen_in_workgroup ciw on z.id = ciw.citizen_id
where z.id = _id
group by z.id
) as t;

View File

@@ -9,7 +9,6 @@ begin
z.*,
find_user_by_id(z.user_id) as "user"
from citizen as z
left join citizen_in_workgroup ciw on z.id = ciw.citizen_id
where z.id = _id
group by z.id
) as t;

View File

@@ -9,6 +9,8 @@ begin
from (
select
com.*,
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count,
find_comment_parent_by_id(com.parent_id) as parent,
find_reference_by_id(com.target_id, com.target_reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes

View File

@@ -0,0 +1,20 @@
create or replace function find_comment_parent_by_id(
_parent_id uuid,
out resource json
) language plpgsql as
$$
begin
select to_json(t)
into resource
from (
select
id,
deleted_at,
json_build_object('id', target_id, 'reference', target_reference) as target
from "comment" cp
where id = _parent_id
) as t;
end;
$$;

View File

@@ -20,6 +20,8 @@ begin
from (
select
com.*,
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count,
find_comment_parent_by_id(com.parent_id) as parent,
find_reference_by_id(com.target_id, _reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes

View File

@@ -13,6 +13,7 @@ begin
select
com.*,
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count,
find_comment_parent_by_id(com.parent_id) as parent,
find_reference_by_id(com.target_id, com.target_reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes

View File

@@ -14,6 +14,7 @@ begin
select
com.*,
(select count(c2) from "comment" c2 where c2.parent_comment_id = com.id) as children_count,
find_comment_parent_by_id(com.parent_id) as parent,
find_reference_by_id(com.target_id, com.target_reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes

View File

@@ -0,0 +1,5 @@
create or replace function json_to_array(json json) returns text[] language sql
immutable parallel safe as
$$
select array(select json_array_elements_text(json))
$$;

View File

@@ -1,31 +1,32 @@
create or replace function vote(reference regclass, _target_id uuid, _created_by_id uuid, _note int, _anonymous bool default true, out resource json)
create or replace function vote(reference regclass, _target_id uuid, _created_by_id uuid, _note int, _anonymous bool default null, out resource json)
language plpgsql as
$$
declare _anonymous_conf bool = coalesce(_anonymous, (select vote_anonymous from citizen where id = _created_by_id));
begin
if reference = 'article'::regclass then
insert into vote_for_article (created_by_id, target_id, note, anonymous)
values (_created_by_id, _target_id, _note, _anonymous)
values (_created_by_id, _target_id, _note, _anonymous_conf)
on conflict (created_by_id, target_id) do update set
note = excluded.note,
anonymous = excluded.anonymous,
updated_at = now();
elseif reference = 'constitution'::regclass then
insert into vote_for_constitution (created_by_id, target_id, note, anonymous)
values (_created_by_id, _target_id, _note, _anonymous)
values (_created_by_id, _target_id, _note, _anonymous_conf)
on conflict (created_by_id, target_id) do update set
note = excluded.note,
anonymous = excluded.anonymous,
updated_at = now();
elseif reference = 'comment_on_article'::regclass then
insert into vote_for_comment_on_article (created_by_id, target_id, note, anonymous)
values (_created_by_id, _target_id, _note, _anonymous)
values (_created_by_id, _target_id, _note, _anonymous_conf)
on conflict (created_by_id, target_id) do update set
note = excluded.note,
anonymous = excluded.anonymous,
updated_at = now();
elseif reference = 'comment_on_constitution'::regclass then
insert into vote_for_comment_on_constitution (created_by_id, target_id, note, anonymous)
values (_created_by_id, _target_id, _note, _anonymous)
values (_created_by_id, _target_id, _note, _anonymous_conf)
on conflict (created_by_id, target_id) do update set
note = excluded.note,
anonymous = excluded.anonymous,

View File

@@ -23,9 +23,9 @@ begin
insert into citizen_in_workgroup (workgroup_id, citizen_id, roles)
select
new_id::uuid,
citizen_id,
roles
from json_populate_recordset(null::citizen_in_workgroup, resource->'members') m;
(m#>>'{citizen,id}')::uuid,
json_to_array(m#>'{roles}')
from json_array_elements(resource->'members') m;
-- insert master if no members
if (exists) then

View File

@@ -11,6 +11,7 @@ drop table if exists follow_constitution;
drop table if exists follow_citizen;
drop table if exists follow;
drop table if exists vote_cache;
drop table if exists vote_for_article;
drop table if exists vote_for_constitution;
drop table if exists vote_for_comment_on_article;

View File

@@ -1,101 +0,0 @@
import fr.dcproject.entity.Article
import fr.dcproject.entity.CitizenBasic
import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.User
import fr.postgresjson.serializer.deserialize
import fr.postgresjson.serializer.serialize
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.util.KtorExperimentalAPI
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.shouldBe
import org.intellij.lang.annotations.Language
import org.joda.time.DateTime
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
@KtorExperimentalLocationsAPI
@KtorExperimentalAPI
@TestInstance(PER_CLASS)
class ArticleTest {
@Language("JSON")
private val articleJson: String = """
{
"id": "83b0b60a-5ab3-44f2-b243-1dc469a7564f",
"title": "Hello world!",
"anonymous": true,
"content": "bla bla bla",
"description": "this is the changement !",
"tags": [],
"draft": false,
"last_version": false,
"created_by": {
"id": "94a0d350-7eab-4a6e-9f84-0c2e7635b67c",
"name": {
"first_name": "Jaque",
"last_name": "Bono",
"civility": null
},
"email": "jaque.bono@gmail.com",
"birthday": "2020-03-16T01:48:27.020Z",
"vote_anonymous": true,
"follow_anonymous": true,
"user": {
"id": "2bc356a2-4d3e-46ff-91f4-ae30fb7fa67d",
"username": "jaque",
"blocked_at": null,
"plain_password": "azerty",
"roles": [],
"created_at": "2020-03-16T01:48:24.153Z",
"updated_at": "2020-03-16T01:48:24.516Z"
},
"deleted_at": null,
"deleted": false
},
"reference": "article",
"views": {
"total": 0,
"unique": 0,
"updated_at": "2020-03-16T01:48:31.070Z"
},
"version_id": "27cb4f5d-d425-4e10-95ca-6c50fac73408",
"version_number": null,
"created_at": "2020-03-16T01:48:31.004Z",
"deleted_at": null,
"deleted": false,
"votes": {
"up": 0,
"neutral": 0,
"down": 0,
"total": 0,
"score": 0,
"updated_at": null
},
"opinions": {}
}
""".trimIndent()
@Test
fun `test Article serialize`() {
val user = User(username = "jaque", plainPassword = "azerty")
val citizen = CitizenBasic(
name = CitizenI.Name("Jaque", "Bono"),
birthday = DateTime.now(),
email = "jaque.bono@gmail.com",
user = user
)
val article = Article(
title = "Hello world!",
content = "bla bla bla",
description = "this is the changement !",
createdBy = citizen
)
article.serialize().contains("""Hello world!""") shouldBe true
}
@Test
fun `test Article Deserialize`() {
val article2: Article = articleJson.deserialize()!!
article2.id.toString() `should be equal to` "83b0b60a-5ab3-44f2-b243-1dc469a7564f"
}
}

Some files were not shown because too many files have changed in this diff Show More