Big refactoring #77

Merged
flecomte merged 166 commits from refactoring-component-and-immutable into master 2021-03-24 19:06:07 +01:00
124 changed files with 2026 additions and 1828 deletions
Showing only changes of commit a1c1accc87 - Show all commits

2
.env
View File

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

View File

@@ -2,28 +2,10 @@
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" /> <option name="LINE_SEPARATOR" value="&#10;" />
<JetCodeStyleSettings> <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" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<PostgresCodeStyleSettings version="2"> <PostgresCodeStyleSettings version="5">
<option name="myVersion" value="2" /> <option name="USE_GENERAL_STYLE" value="false" />
<option name="KEYWORD_CASE" value="1" /> <option name="KEYWORD_CASE" value="1" />
<option name="IDENTIFIER_CASE" value="1" /> <option name="IDENTIFIER_CASE" value="1" />
<option name="TYPE_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" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<module name="dcproject.test" /> <module name="dcproject.test" />
<useClassPathOnly /> <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="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="RunCucumberTest" /> <option name="MAIN_CLASS_NAME" value="RunCucumberTest" />
<option name="METHOD_NAME" value="" /> <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"> <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" /> <output_file path="$PROJECT_DIR$/var/log/test/out.log" is_save="true" />
<useClassPathOnly /> <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="PACKAGE_NAME" value="fr.dcproject" />
<option name="MAIN_CLASS_NAME" value="" /> <option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />

View File

@@ -15,12 +15,12 @@ help: ## This help.
bd: build-docker 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 . docker build -t dc-project -f docker/app/Dockerfile .
pd: publish-docker 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) @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 @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} @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 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 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 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) @git diff --quiet --exit-code || (echo "The git is DIRTY !!! You cannot publish this crap!" && exit 1)
gradlew publish gradlew publish
f: fixtures f: fixtures
fixtures: ## Import fixtures fixtures: ## Import fixtures (alias: f)
bash src/main/resources/sql/fixtures/fixtures.sh bash src/main/resources/sql/fixtures/fixtures.sh
reset-database: ## Import fixtures reset-database: ## Reset database !!!
cd src/main/resources/sql/ ; bash resetDB.sh cd src/main/resources/sql/ ; bash resetDB.sh
test-sql: ## Test sql test-sql: ## Test sql
cd src/test/sql/ ; bash test.sh 1 cd src/test/sql/ ; bash test.sh 1
v: vertion v: version
vertion: ## Show current version version: ## Show current version (alias: v)
@echo ${VERSION} @echo ${VERSION}

View File

@@ -23,6 +23,7 @@ version = versioning.info.run {
plugins { plugins {
jacoco jacoco
application application
maven
id("maven-publish") id("maven-publish")
id("org.jetbrains.kotlin.jvm") version "1.3.50" id("org.jetbrains.kotlin.jvm") version "1.3.50"
@@ -138,8 +139,8 @@ dependencies {
implementation("net.pearx.kasechange:kasechange-jvm:1.1.0") implementation("net.pearx.kasechange:kasechange-jvm:1.1.0")
implementation("com.auth0:java-jwt:3.8.2") implementation("com.auth0:java-jwt:3.8.2")
implementation("com.github.jasync-sql:jasync-postgresql:1.0.7") implementation("com.github.jasync-sql:jasync-postgresql:1.0.7")
implementation("com.github.flecomte:postgres-json:1.2.1") implementation("com.github.flecomte:postgres-json:2.0.0")
implementation("com.github.flecomte:ktor-voter:2.2.1") implementation("com.github.flecomte:ktor-voter:3.0.0")
implementation("com.sendgrid:sendgrid-java:4.4.1") implementation("com.sendgrid:sendgrid-java:4.4.1")
implementation("io.lettuce:lettuce-core:5.2.2.RELEASE") implementation("io.lettuce:lettuce-core:5.2.2.RELEASE")
implementation("com.rabbitmq:amqp-client:5.8.0") 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' version: '3.7'
services: services:
sonarqube: sonarqube:
container_name: sonarqube_${NAME} container_name: ${APP_NAME}_sonarqube
image: sonarqube image: sonarqube
ports: ports:
- ${SONARQUBE_PORT}:9000 - ${SONARQUBE_PORT}:9000
openapi: openapi:
container_name: openapi_${NAME} container_name: ${APP_NAME}_openapi
image: swaggerapi/swagger-ui image: swaggerapi/swagger-ui
ports: ports:
- ${OPENAPI_PORT}:8080 - ${OPENAPI_PORT}:8080
@@ -17,14 +17,14 @@ services:
URL: "http://localhost:8080" URL: "http://localhost:8080"
rabbitmq: rabbitmq:
container_name: rabbitmq_${NAME} container_name: ${APP_NAME}_rabbitmq
image: rabbitmq:management-alpine image: rabbitmq:management-alpine
ports: ports:
- ${RABBITMQ_PORT}:5672 - ${RABBITMQ_PORT}:5672
- ${RABBITMQ_MANAGEMENT_PORT}:15672 - ${RABBITMQ_MANAGEMENT_PORT}:15672
redis: redis:
container_name: redis_${NAME} container_name: ${APP_NAME}_redis
image: redis:6.0-rc-alpine image: redis:6.0-rc-alpine
ports: ports:
- ${REDIS_PORT}:6379 - ${REDIS_PORT}:6379
@@ -32,7 +32,7 @@ services:
- redis-data:/var/lib/redis:rw - redis-data:/var/lib/redis:rw
app: app:
container_name: app_${NAME} container_name: ${APP_NAME}_app
build: build:
context: . context: .
dockerfile: docker/app/Dockerfile dockerfile: docker/app/Dockerfile
@@ -51,7 +51,7 @@ services:
- rabbitmq - rabbitmq
elasticsearch: elasticsearch:
container_name: elasticsearch_${NAME} container_name: ${APP_NAME}_elasticsearch
image: elasticsearch:6.7.1 image: elasticsearch:6.7.1
ports: ports:
- ${ELASTIC_REST}:9200 - ${ELASTIC_REST}:9200
@@ -63,7 +63,7 @@ services:
retries: 20 retries: 20
db: db:
container_name: postgresql_${NAME} container_name: ${APP_NAME}_postgresql
build: build:
context: docker/postgresql context: docker/postgresql
ports: ports:

View File

@@ -4,6 +4,7 @@ COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src WORKDIR /home/gradle/src
RUN gradle build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck --no-daemon RUN gradle build -x test -x ktlintKotlinScriptCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck --no-daemon
RUN gradle shadowJar
#### RUN #### #### RUN ####
FROM adoptopenjdk/openjdk11:jre-11.0.4_11-alpine 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.fasterxml.jackson.datatype.joda.JodaModule
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import fr.dcproject.Env.PROD 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.elasticsearch.configElasticIndexes
import fr.dcproject.entity.* import fr.dcproject.entity.User
import fr.dcproject.event.EventNotification import fr.dcproject.event.EventNotification
import fr.dcproject.event.EventSubscriber import fr.dcproject.event.EventSubscriber
import fr.dcproject.routes.* import fr.dcproject.routes.*
import fr.dcproject.security.voter.* import fr.dcproject.security.voter.*
import fr.ktorVoter.AuthorizationVoter import fr.ktorVoter.AuthorizationVoter
import fr.ktorVoter.ForbiddenException import fr.ktorVoter.VoterException
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.*
import io.ktor.application.ApplicationCall import io.ktor.auth.*
import io.ktor.application.call import io.ktor.auth.jwt.*
import io.ktor.application.install import io.ktor.client.*
import io.ktor.auth.Authentication import io.ktor.client.engine.jetty.*
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.features.* import io.ktor.features.*
import io.ktor.http.HttpHeaders import io.ktor.http.*
import io.ktor.http.HttpMethod import io.ktor.http.auth.*
import io.ktor.http.HttpStatusCode import io.ktor.jackson.*
import io.ktor.http.auth.HttpAuthHeader import io.ktor.locations.*
import io.ktor.jackson.jackson import io.ktor.response.*
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.routing.*
import io.ktor.locations.Locations import io.ktor.util.*
import io.ktor.response.respond import io.ktor.websocket.*
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog import org.eclipse.jetty.util.log.Slf4jLog
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
@@ -47,13 +44,7 @@ import org.slf4j.event.Level
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.CompletionException 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.User as UserRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
fun main(args: Array<String>): Unit = io.ktor.server.jetty.EngineMain.main(args) 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) { fun Application.module(env: Env = PROD) {
install(Koin) { install(Koin) {
Slf4jLog() Slf4jLog()
modules(Module) modules(KoinModule)
} }
install(CallLogging) { install(CallLogging) {
level = Level.INFO level = Level.INFO
} }
install(DataConversion) { install(DataConversion, converters)
convert<UUID> {
decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) }
}
encode { value -> install(Locations)
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(AuthorizationVoter) { install(AuthorizationVoter) {
voters = listOf( voters = listOf(
ArticleVoter(get()),
ConstitutionVoter(), ConstitutionVoter(),
CitizenVoter(), CitizenVoter(),
CommentVoter(), CommentVoter(),
@@ -255,10 +148,13 @@ fun Application.module(env: Env = PROD) {
} }
} }
install(Routing) { install(Routing.Feature) {
// trace { application.log.trace(it.buildText()) } // trace { application.log.trace(it.buildText()) }
authenticate(optional = true) { authenticate(optional = true) {
article(get(), get()) findArticles(get(), get())
getOneArticle(get(), get())
upsertArticle(get(), get(), get())
findArticleVersions(get(), get())
auth(get(), get(), get()) auth(get(), get(), get())
citizen(get(), get()) citizen(get(), get())
constitution(get()) constitution(get())
@@ -293,6 +189,10 @@ fun Application.module(env: Env = PROD) {
exception<NotFoundException> { e -> exception<NotFoundException> { e ->
call.respond(HttpStatusCode.NotFound, e.message!!) call.respond(HttpStatusCode.NotFound, e.message!!)
} }
exception<VoterException> {
if (call.user == null) call.respond(HttpStatusCode.Unauthorized)
else call.respond(HttpStatusCode.Forbidden)
}
exception<ForbiddenException> { exception<ForbiddenException> {
call.respond(HttpStatusCode.Forbidden) call.respond(HttpStatusCode.Forbidden)
} }

View File

@@ -2,20 +2,21 @@ package fr.dcproject
import fr.dcproject.entity.User import fr.dcproject.entity.User
import fr.dcproject.entity.UserI import fr.dcproject.entity.UserI
import fr.ktorVoter.ForbiddenException import io.ktor.application.*
import io.ktor.application.ApplicationCall import io.ktor.auth.*
import io.ktor.auth.authentication import io.ktor.util.*
import io.ktor.util.AttributeKey import io.ktor.util.pipeline.*
import io.ktor.util.pipeline.PipelineContext
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import fr.dcproject.entity.Citizen as CitizenEntity import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Citizen as CitizenRepository
class ForbiddenException(message: String) : Exception(message)
private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext") private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
val ApplicationCall.citizen: CitizenEntity val ApplicationCall.citizen: CitizenEntity
get() = attributes.computeIfAbsent(citizenAttributeKey) { 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) GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"") ?: 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.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.rabbitmq.client.ConnectionFactory 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.event.publisher.Publisher
import fr.dcproject.messages.Mailer import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender import fr.dcproject.messages.NotificationEmailSender
import fr.dcproject.messages.SsoManager import fr.dcproject.messages.SsoManager
import fr.dcproject.views.ArticleViewManager
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
@@ -25,7 +27,6 @@ import org.apache.http.HttpHost
import org.elasticsearch.client.RestClient import org.elasticsearch.client.RestClient
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.CommentArticle as CommentArticleRepository import fr.dcproject.repository.CommentArticle as CommentArticleRepository
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository 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 import fr.dcproject.repository.Workgroup as WorkgroupRepository
@KtorExperimentalAPI @KtorExperimentalAPI
val Module = module { val KoinModule = module {
single { Config } single { Config }
@@ -114,6 +115,9 @@ val Module = module {
single { OpinionArticleRepository(get()) } single { OpinionArticleRepository(get()) }
single { WorkgroupRepository(get()) } single { WorkgroupRepository(get()) }
// Voters
single { ArticleVoter(get()) }
// Elasticsearch Client // Elasticsearch Client
single<RestClient> { single<RestClient> {
RestClient.builder( 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.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter import fr.postgresjson.entity.Parameter
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase import net.pearx.kasechange.toSnakeCase
import java.util.* import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
class Article(override var requester: Requester) : RepositoryI { class ArticleRepository(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): ArticleEntity? { fun findById(id: UUID): ArticleForView? {
val function = requester.getFunction("find_article_by_id") val function = requester.getFunction("find_article_by_id")
return function.selectOne("id" to 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 return requester
.getFunction("find_articles_versions_by_version_id") .getFunction("find_articles_versions_by_version_id")
.select(page, limit, "version_id" to versionId) .select(page, limit, "version_id" to versionId)
@@ -27,10 +23,10 @@ class Article(override var requester: Requester) : RepositoryI {
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
sort: String? = null, sort: String? = null,
direction: Direction? = null, direction: RepositoryI.Direction? = null,
search: String? = null, search: String? = null,
filter: Filter = Filter() filter: Filter = Filter()
): Paginated<ArticleSimple> { ): Paginated<ArticleForListing> {
return requester return requester
.getFunction("find_articles") .getFunction("find_articles")
.select( .select(
@@ -42,7 +38,7 @@ class Article(override var requester: Requester) : RepositoryI {
) )
} }
fun upsert(article: ArticleForUpdate): ArticleEntity? { fun upsert(article: ArticleForUpdate): ArticleForView? {
return requester return requester
.getFunction("upsert_article") .getFunction("upsert_article")
.selectOne("resource" to article) .selectOne("resource" to article)

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.contentToString
import fr.dcproject.utils.getJsonField import fr.dcproject.utils.getJsonField
import fr.dcproject.utils.toIso import fr.dcproject.utils.toIso
import fr.dcproject.views.ViewManager
import org.elasticsearch.client.Request import org.elasticsearch.client.Request
import org.elasticsearch.client.Response import org.elasticsearch.client.Response
import org.elasticsearch.client.RestClient import org.elasticsearch.client.RestClient
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* 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 isLogged = (citizen != null).toString()
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!! val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
val request = Request( val request = Request(
@@ -36,7 +44,10 @@ class ArticleViewManager(private val restClient: RestClient) : ViewManager<Artic
return restClient.performRequest(request) 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( val request = Request(
"GET", "GET",
"/views/_search" "/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) { fun waitElasticsearchIsUp(client: RestClient) {
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch") val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch")
val request = Request("GET", "/_cluster/health") val request = Request("GET", "/_cluster/health")
repeat(40) { repeat(5*60/2) { // 5 minutes
runCatching { runCatching {
client.performRequest(request).statusLine.statusCode client.performRequest(request).statusLine.statusCode
}.onSuccess { }.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 package fr.dcproject.entity
import fr.dcproject.entity.CitizenI.Name import fr.dcproject.entity.CitizenI.Name
import fr.postgresjson.entity.immutable.EntityCreatedAt import fr.postgresjson.entity.*
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 org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
@Deprecated("")
class Citizen( class Citizen(
id: UUID = UUID.randomUUID(), override val id: UUID = UUID.randomUUID(),
name: Name, override val name: Name,
email: String, override val email: String,
birthday: DateTime, override val birthday: DateTime,
voteAnonymous: Boolean = true, override val voteAnonymous: Boolean = true,
followAnonymous: Boolean = true, override val followAnonymous: Boolean = true,
override val user: User override val user: User,
deletedAt: DateTime? = null
) : CitizenFull, ) : CitizenFull,
CitizenBasic(id, name, email, birthday, voteAnonymous, followAnonymous, user), CitizenBasicI,
EntityCreatedAt by EntityCreatedAtImp() { CitizenRef(id),
CitizenCartI,
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(deletedAt) {
var workgroups: List<WorkgroupAndRoles> = emptyList() var workgroups: List<WorkgroupAndRoles> = emptyList()
class WorkgroupAndRoles( class WorkgroupAndRoles(
@@ -29,29 +29,43 @@ class Citizen(
) )
} }
open class CitizenBasic( @Deprecated("")
id: UUID = UUID.randomUUID(), data class CitizenBasic(
name: Name, override var id: UUID = UUID.randomUUID(),
override var name: Name,
override var email: String, override var email: String,
override var birthday: DateTime, override var birthday: DateTime,
override var voteAnonymous: Boolean = true, override var voteAnonymous: Boolean = true,
override var followAnonymous: Boolean = true, override var followAnonymous: Boolean = true,
override val user: User override val user: User,
override val deletedAt: DateTime? = null
) : CitizenBasicI, ) : CitizenBasicI,
CitizenSimple(id, name, user) CitizenRefWithUser(id, user),
EntityDeletedAt by EntityDeletedAtImp(deletedAt)
@Deprecated("")
open class CitizenSimple( open class CitizenSimple(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
var name: Name, var name: Name,
user: UserRef user: UserRef
) : CitizenRefWithUser(id, user) ) : 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( open class CitizenRefWithUser(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val user: UserRef override val user: UserRef
) : CitizenWithUserI, ) : CitizenWithUserI,
CitizenRef(id), CitizenRef(id)
EntityDeletedAt by EntityDeletedAtImp()
open class CitizenRef( open class CitizenRef(
id: UUID = UUID.randomUUID() id: UUID = UUID.randomUUID()
@@ -60,22 +74,29 @@ open class CitizenRef(
interface CitizenI : UuidEntityI { interface CitizenI : UuidEntityI {
data class Name( data class Name(
var firstName: String, override val firstName: String,
var lastName: String, override val lastName: String,
var civility: String? = null override val civility: String? = null
) { ) : NameI
interface NameI {
val firstName: String
val lastName: String
val civility: String?
fun getFullName(): String = "${civility ?: ""} $firstName $lastName".trim() fun getFullName(): String = "${civility ?: ""} $firstName $lastName".trim()
} }
} }
@Deprecated("")
interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt { interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt {
var name: Name val name: Name
var email: String val email: String
var birthday: DateTime val birthday: DateTime
var voteAnonymous: Boolean val voteAnonymous: Boolean
var followAnonymous: Boolean val followAnonymous: Boolean
} }
@Deprecated("")
interface CitizenFull : CitizenBasicI { interface CitizenFull : CitizenBasicI {
override val user: User override val user: User
} }

View File

@@ -1,30 +1,28 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.immutable.* import fr.postgresjson.entity.*
import fr.postgresjson.entity.mutable.EntityDeletedAt import org.joda.time.DateTime
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.* import java.util.*
open class Comment<T : TargetI>( class CommentForView<T : TargetI, C : CitizenRef>(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic, override val createdBy: C,
override var target: T, override val target: T,
var content: String, override var content: String,
val responses: List<Comment<T>>? = null, override val parent: CommentParent<T>? = null,
var parent: Comment<T>? = null, val childrenCount: Int? = null,
val parentsIds: List<UUID>? = null, override val deletedAt: DateTime? = null
val childrenCount: Int? = null ) : ExtraI<T, C>,
) : ExtraI<T, CitizenBasicI>, CommentForUpdate<T, C>(id, createdBy, target, content, parent, deletedAt),
CommentRef(id), CommentWithTargetI<T>,
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedBy<C> by EntityCreatedByImp(createdBy),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp(), EntityUpdatedAt by EntityUpdatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(), EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(), Votable by VotableImp(),
TargetI { TargetI {
constructor( constructor(
createdBy: CitizenBasic, createdBy: C,
parent: Comment<T>, parent: CommentParent<T>,
content: String content: String
) : this( ) : this(
createdBy = createdBy, 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 package fr.dcproject.entity
import fr.postgresjson.entity.immutable.* import fr.dcproject.component.article.ArticleI
import fr.postgresjson.entity.mutable.EntityDeletedAt import fr.dcproject.component.article.ArticleSimple
import fr.postgresjson.entity.mutable.EntityDeletedAtImp import fr.postgresjson.entity.*
import fr.postgresjson.entity.mutable.EntityVersioning
import fr.postgresjson.entity.mutable.UuidEntityVersioning
import java.util.* import java.util.*
class Constitution( class Constitution(
@@ -33,17 +31,17 @@ class Constitution(
) : ConstitutionSimple.TitleSimple<ArticleSimple>(id, name, rank) ) : 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(), id: UUID = UUID.randomUUID(),
var title: String, val title: String,
var anonymous: Boolean = true, val anonymous: Boolean = true,
open var titles: MutableList<T> = mutableListOf(), val titles: MutableList<T> = mutableListOf(),
var draft: Boolean = false, val draft: Boolean = false,
var lastVersion: Boolean = false, val lastVersion: Boolean = false,
override val createdBy: Cr, override val createdBy: Cr,
versionId: UUID = UUID.randomUUID() versionId: UUID = UUID.randomUUID()
) : ConstitutionRef(id), ) : ConstitutionRef(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionId = versionId), EntityVersioning<UUID, Int> by UuidEntityVersioning(versionId = versionId, versionNumber = 0),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<Cr> by EntityCreatedByImp(createdBy), EntityCreatedBy<Cr> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp() { 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 package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt import fr.dcproject.component.article.ArticleRef
import fr.postgresjson.entity.immutable.EntityCreatedBy import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.immutable.UuidEntity import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.immutable.UuidEntityI import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.entity.UuidEntityI
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
interface ExtraI<T : TargetI, C : CitizenI> : interface ExtraI<T : TargetI, C : CitizenI> :
UuidEntityI, UuidEntityI,
AsTarget<T>,
EntityCreatedAt, EntityCreatedAt,
EntityCreatedBy<C> { EntityCreatedBy<C>
interface AsTarget<T: TargetI> {
val target: T val target: T
} }

View File

@@ -1,8 +1,9 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.immutable.* import fr.postgresjson.entity.*
import java.util.* import java.util.*
@Deprecated("")
class Follow<T : TargetI>( class Follow<T : TargetI>(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic, override val createdBy: CitizenBasic,
@@ -10,11 +11,26 @@ class Follow<T : TargetI>(
) : ExtraI<T, CitizenBasicI>, ) : ExtraI<T, CitizenBasicI>,
FollowSimple<T, CitizenBasicI>(id, createdBy, target) FollowSimple<T, CitizenBasicI>(id, createdBy, target)
@Deprecated("")
open class FollowSimple<T : TargetI, C : CitizenI>( open class FollowSimple<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: C, override val createdBy: C,
override var target: T override var target: T
) : ExtraI<T, C>, ) : ExtraI<T, C>,
UuidEntity(id), FollowRef(id),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C> by EntityCreatedByImp(createdBy) 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 package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt import fr.dcproject.component.article.ArticleRef
import fr.postgresjson.entity.immutable.EntityCreatedAtImp import fr.postgresjson.entity.*
import fr.postgresjson.entity.immutable.EntityCreatedBy
import fr.postgresjson.entity.immutable.EntityCreatedByImp
import java.util.* import java.util.*
@Deprecated("")
open class Opinion<T : TargetI>( open class Opinion<T : TargetI>(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic, override val createdBy: CitizenBasic,
@@ -19,9 +18,24 @@ open class Opinion<T : TargetI>(
fun getName(): String = choice.name fun getName(): String = choice.name
} }
@Deprecated("")
class OpinionArticle( class OpinionArticle(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
createdBy: CitizenBasic, createdBy: CitizenBasic,
target: ArticleRef, target: ArticleRef,
choice: OpinionChoice 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 package fr.dcproject.entity
import fr.postgresjson.entity.immutable.EntityCreatedAt import fr.postgresjson.entity.*
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
import fr.postgresjson.entity.immutable.UuidEntity
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.* import java.util.*
class OpinionChoice( class OpinionChoice(

View File

@@ -1,15 +1,12 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.EntityI typealias Opinions = Map<String, Int>
typealias OpinionsMutable = MutableMap<String, Int>
class OpinionAggregation(
private val underlying: MutableMap<String, Any> = mutableMapOf()
) : MutableMap<String, Any> by underlying, EntityI
interface Opinionable { interface Opinionable {
var opinions: MutableMap<String, Int> val opinions: Opinions
} }
class OpinionableImp : Opinionable { 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 package fr.dcproject.entity
import fr.dcproject.entity.UserI.Roles import fr.dcproject.entity.UserI.Roles
import fr.postgresjson.entity.immutable.* import fr.postgresjson.entity.*
import io.ktor.auth.Principal import io.ktor.auth.*
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
@Deprecated("")
class User( class User(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
username: String, username: String,
@@ -16,6 +17,7 @@ class User(
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp() EntityUpdatedAt by EntityUpdatedAtImp()
@Deprecated("")
open class UserBasic( open class UserBasic(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
override var username: String, override var username: String,
@@ -30,12 +32,19 @@ interface UserI : UuidEntityI, Principal {
enum class Roles { ROLE_USER, ROLE_ADMIN } enum class Roles { ROLE_USER, ROLE_ADMIN }
} }
@Deprecated("")
interface UserBasicI : UserI { interface UserBasicI : UserI {
var username: String var username: String
var blockedAt: DateTime? var blockedAt: DateTime?
} }
@Deprecated("")
interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt { interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt {
var plainPassword: String? var plainPassword: String?
var roles: List<Roles> 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 package fr.dcproject.entity
import fr.postgresjson.entity.EntityI import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.immutable.EntityUpdatedAt import fr.postgresjson.entity.EntityUpdatedAt
import fr.postgresjson.entity.immutable.EntityUpdatedAtImp import fr.postgresjson.entity.EntityUpdatedAtImp
open class ViewAggregation( class ViewAggregation(
val total: Int, val total: Int,
val unique: Int val unique: Int
) : EntityI, ) : 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 package fr.dcproject.entity
interface Votable { interface Votable {
var votes: VoteAggregation val votes: VoteAggregation
} }
class VotableImp : Votable { class VotableImp : Votable {
override var votes: VoteAggregation = VoteAggregation() override val votes: VoteAggregation = VoteAggregation()
} }

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
package fr.dcproject.event package fr.dcproject.event
import fr.postgresjson.entity.Serializable import fr.postgresjson.entity.Serializable
import fr.postgresjson.entity.immutable.UuidEntity import fr.postgresjson.entity.UuidEntity
import io.ktor.application.* import io.ktor.application.*
import io.ktor.util.AttributeKey import io.ktor.util.*
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import org.joda.time.DateTime import org.joda.time.DateTime
import kotlin.random.Random.Default.nextInt 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.Mail
import com.sendgrid.helpers.mail.objects.Content import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.entity.* import fr.dcproject.component.article.ArticleRepository
import fr.postgresjson.entity.immutable.UuidEntityI 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 java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Article as ArticleRepository
class NotificationEmailSender( class NotificationEmailSender(
private val mailer: Mailer, private val mailer: Mailer,
@@ -40,7 +44,7 @@ class NotificationEmailSender(
private fun generateHtmlContent(citizen: CitizenBasicI, target: UuidEntityI): String? { private fun generateHtmlContent(citizen: CitizenBasicI, target: UuidEntityI): String? {
return when (target) { return when (target) {
is Article -> """ is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()},<br/> Hello ${citizen.name.getFullName()},<br/>
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a> The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
""".trimIndent() """.trimIndent()
@@ -50,7 +54,7 @@ class NotificationEmailSender(
private fun generateContent(citizen: CitizenBasicI, target: UuidEntityI): String { private fun generateContent(citizen: CitizenBasicI, target: UuidEntityI): String {
return when (target) { return when (target) {
is Article -> """ is ArticleWithTitleI -> """
Hello ${citizen.name.getFullName()}, Hello ${citizen.name.getFullName()},
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id} The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
""".trimIndent() """.trimIndent()

View File

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

View File

@@ -1,7 +1,7 @@
package fr.dcproject.repository package fr.dcproject.repository
import fr.dcproject.entity.ArticleRef import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenSimple import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.entity.ConstitutionSimple import fr.dcproject.entity.ConstitutionSimple
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester 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 return requester
.getFunction("upsert_constitution") .getFunction("upsert_constitution")
.selectOne("resource" to constitution) .selectOne("resource" to constitution)

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity import fr.dcproject.entity.Workgroup as WorkgroupEntity
class Workgroup(override var requester: Requester) : RepositoryI { 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") val function = requester.getFunction("find_workgroup_by_id")
return function.selectOne("id" to id) return function.selectOne("id" to id)
} }
@@ -25,7 +25,7 @@ class Workgroup(override var requester: Requester) : RepositoryI {
direction: Direction? = null, direction: Direction? = null,
search: String? = null, search: String? = null,
filter: Filter = Filter() filter: Filter = Filter()
): Paginated<WorkgroupEntity> { ): Paginated<WorkgroupEntity<CitizenBasic>> {
return requester return requester
.getFunction("find_workgroups") .getFunction("find_workgroups")
.select( .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") .getFunction("upsert_workgroup")
.selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null") .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") .getFunction("delete_workgroup")
.perform("id" to workgroup.id) .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 package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.entity.Comment import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.CommentRef import fr.dcproject.entity.CommentRef
import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content
import fr.dcproject.security.voter.CommentVoter.Action.CREATE import fr.dcproject.security.voter.CommentVoter.Action.*
import fr.dcproject.security.voter.CommentVoter.Action.UPDATE
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
import fr.ktorVoter.assertCan import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll import fr.ktorVoter.assertCanAll
import io.ktor.application.* import io.ktor.application.*
@@ -67,7 +65,7 @@ fun Route.comment(repo: CommentRepository) {
post<CommentPaths.CreateCommentRequest> { post<CommentPaths.CreateCommentRequest> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = Comment( val newComment = CommentForUpdate(
content = call.receive<Content>().content, content = call.receive<Content>().content,
createdBy = citizen, createdBy = citizen,
parent = parent parent = parent
@@ -83,6 +81,7 @@ fun Route.comment(repo: CommentRepository) {
val comment = repo.findById(it.comment.id)!! val comment = repo.findById(it.comment.id)!!
assertCan(UPDATE, comment) assertCan(UPDATE, comment)
comment.content = call.receiveText() comment.content = call.receiveText()
repo.edit(comment) repo.edit(comment)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package fr.dcproject.routes package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.CitizenRef import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE 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.KoinComponent
import org.koin.core.get import org.koin.core.get
import java.util.* import java.util.*
import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Citizen as CitizenEntity import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@@ -39,7 +39,7 @@ object OpinionArticlePaths {
*/ */
@Location("/articles/{article}/opinions") @Location("/articles/{article}/opinions")
@KtorExperimentalAPI @KtorExperimentalAPI
class ArticleOpinion(val article: ArticleEntity) { class ArticleOpinion(val article: ArticleForView) {
class Body(ids: List<String>) { class Body(ids: List<String>) {
val ids = ids.map { OpinionChoiceRef(it.toUUID()) } val ids = ids.map { OpinionChoiceRef(it.toUUID()) }
} }

View File

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

View File

@@ -2,19 +2,17 @@ package fr.dcproject.routes
import fr.dcproject.citizen import fr.dcproject.citizen
import fr.dcproject.entity.Citizen import fr.dcproject.entity.Citizen
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
import fr.dcproject.security.voter.VoteVoter.Action.CREATE import fr.dcproject.security.voter.VoteVoter.Action.CREATE
import fr.ktorVoter.assertCan import fr.ktorVoter.assertCan
import io.ktor.application.call import io.ktor.application.*
import io.ktor.http.HttpStatusCode import io.ktor.http.*
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.*
import io.ktor.locations.Location import io.ktor.request.*
import io.ktor.locations.put import io.ktor.response.*
import io.ktor.request.receive import io.ktor.routing.*
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.entity.Constitution as ConstitutionEntity import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Vote as VoteEntity
import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@@ -32,7 +30,7 @@ object VoteConstitutionPaths {
fun Route.voteConstitution(repo: VoteConstitutionRepository) { fun Route.voteConstitution(repo: VoteConstitutionRepository) {
put<VoteConstitutionPaths.ConstitutionVoteRequest> { put<VoteConstitutionPaths.ConstitutionVoteRequest> {
val content = call.receive<Content>() val content = call.receive<Content>()
val vote = VoteEntity( val vote = VoteForUpdate(
target = it.constitution, target = it.constitution,
note = content.note, note = content.note,
createdBy = this.citizen 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
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
import fr.dcproject.repository.Workgroup.Filter 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.UPDATE
import fr.dcproject.security.voter.WorkgroupVoter.Action.VIEW
import fr.dcproject.utils.toUUID import fr.dcproject.utils.toUUID
import fr.ktorVoter.assertCan import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll import fr.ktorVoter.assertCanAll
@@ -19,9 +19,10 @@ import io.ktor.locations.*
import io.ktor.request.* import io.ktor.request.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.* import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity import fr.dcproject.repository.Workgroup as WorkgroupRepo
import fr.dcproject.repository.Workgroup as WorkgroupRepository
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.ADD as ADD_MEMBERS 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.REMOVE as REMOVE_MEMBERS
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.UPDATE as UPDATE_MEMBERS import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.UPDATE as UPDATE_MEMBERS
@@ -43,8 +44,11 @@ object WorkgroupsPaths {
val members: List<UUID>? = members?.toUUID() val members: List<UUID>? = members?.toUUID()
} }
@Location("/workgroups/{workgroup}") @Location("/workgroups/{workgroupId}")
class WorkgroupRequest(val workgroup: WorkgroupEntity) class WorkgroupRequest(private val workgroupId: UUID) : KoinComponent {
val repo: WorkgroupRepo by inject()
val workgroup = repo.findById(workgroupId) ?: TODO()
}
@Location("/workgroups") @Location("/workgroups")
open class PostWorkgroupRequest { open class PostWorkgroupRequest {
@@ -68,31 +72,30 @@ object WorkgroupsPaths {
} }
} }
@Location("/workgroups/{workgroup}") @Location("/workgroups/{workgroupId}")
class PutWorkgroupRequest(val workgroup: WorkgroupEntity) { class PutWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
class Body( class Input(
val name: String?, val name: String?,
val description: String?, val description: String?,
val logo: String?, val logo: String?,
val anonymous: Boolean? 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}") @Location("/workgroups/{workgroupId}")
class DeleteWorkgroupRequest(val workgroup: WorkgroupEntity) class DeleteWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
val repo: WorkgroupRepo by inject()
val workgroup = repo.findById(workgroupId)
}
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object WorkgroupsMembersPaths { object WorkgroupsMembersPaths {
@Location("/workgroups/{workgroup}/members") @Location("/workgroups/{workgroupId}/members")
class WorkgroupsMembersRequest(val workgroup: WorkgroupEntity) { class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
val repo: WorkgroupRepo by inject()
val workgroup = repo.findById(workgroupId)
class Body : MutableList<Body.Item> by mutableListOf() { class Body : MutableList<Body.Item> by mutableListOf() {
class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) { class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) {
val roles: List<Role> = roles.map { val roles: List<Role> = roles.map {
@@ -111,7 +114,7 @@ object WorkgroupsMembersPaths {
} }
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
fun Route.workgroup(repo: WorkgroupRepository) { fun Route.workgroup(repo: WorkgroupRepo) {
get<WorkgroupsPaths.WorkgroupsRequest> { get<WorkgroupsPaths.WorkgroupsRequest> {
val workgroups = val workgroups =
repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy, members = it.members)) repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy, members = it.members))
@@ -136,22 +139,35 @@ fun Route.workgroup(repo: WorkgroupRepository) {
} }
put<WorkgroupsPaths.PutWorkgroupRequest> { put<WorkgroupsPaths.PutWorkgroupRequest> {
it.updateWorkgroup(call).let { workgroup -> 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) assertCan(UPDATE, workgroup)
repo.upsert(workgroup as WorkgroupSimple<CitizenRef>) repo.upsert(workgroup)
}.let {
call.respond(HttpStatusCode.OK, it) call.respond(HttpStatusCode.OK, it)
} }
} }
} ?: call.respond(HttpStatusCode.NotFound)
}
delete<WorkgroupsPaths.DeleteWorkgroupRequest> { delete<WorkgroupsPaths.DeleteWorkgroupRequest> {
assertCan(UPDATE, it.workgroup) if (it.workgroup != null) {
assertCan(DELETE, it.workgroup)
repo.delete(it.workgroup) repo.delete(it.workgroup)
call.respond(HttpStatusCode.NoContent, it) call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound)
}
} }
/* Add members to workgroup */ /* Add members to workgroup */
post<WorkgroupsMembersPaths.WorkgroupsMembersRequest> { post<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
if (it.workgroup != null) {
it.getMembers(call) it.getMembers(call)
.let { members -> .let { members ->
assertCan(ADD_MEMBERS, it.workgroup) assertCan(ADD_MEMBERS, it.workgroup)
@@ -159,10 +175,14 @@ fun Route.workgroup(repo: WorkgroupRepository) {
}.let { members -> }.let { members ->
call.respond(HttpStatusCode.Created, members) call.respond(HttpStatusCode.Created, members)
} }
} else {
call.respond(HttpStatusCode.NotFound)
}
} }
/* Delete members of workgroup */ /* Delete members of workgroup */
delete<WorkgroupsMembersPaths.WorkgroupsMembersRequest> { delete<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
if (it.workgroup != null) {
it.getMembers(call) it.getMembers(call)
.let { members -> .let { members ->
assertCan(REMOVE_MEMBERS, it.workgroup) assertCan(REMOVE_MEMBERS, it.workgroup)
@@ -170,10 +190,14 @@ fun Route.workgroup(repo: WorkgroupRepository) {
}.let { members -> }.let { members ->
call.respond(HttpStatusCode.OK, members) call.respond(HttpStatusCode.OK, members)
} }
} else {
call.respond(HttpStatusCode.NotFound)
}
} }
/* Update members of workgroup */ /* Update members of workgroup */
put<WorkgroupsMembersPaths.WorkgroupsMembersRequest> { put<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
if (it.workgroup != null) {
it.getMembers(call) it.getMembers(call)
.let { members -> .let { members ->
assertCan(UPDATE_MEMBERS, it.workgroup) assertCan(UPDATE_MEMBERS, it.workgroup)
@@ -181,5 +205,8 @@ fun Route.workgroup(repo: WorkgroupRepository) {
}.let { members -> }.let { members ->
call.respond(HttpStatusCode.OK, 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.DateTime
import org.joda.time.format.ISODateTimeFormat 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 import kotlin.reflect.KProperty
internal class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> { 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 package fr.dcproject.utils
fun String.readResource(callbak: (String) -> Unit = {}): String { fun String.readResource(callback: (String) -> Unit = {}): String {
val content = callbak::class.java.getResource(this).readText() val content = callback::class.java.getResource(this).readText()
callbak(content) callback(content)
return content return content
} }

View File

@@ -1,11 +1,18 @@
package fr.dcproject.views package fr.dcproject.views
import fr.dcproject.entity.CitizenRef import fr.dcproject.entity.CitizenI
import fr.dcproject.entity.ViewAggregation import fr.dcproject.entity.ViewAggregation
import org.elasticsearch.client.Response import org.elasticsearch.client.Response
import org.joda.time.DateTime import org.joda.time.DateTime
interface ViewManager <T> { 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 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 package fr.dcproject.security.voter
import fr.dcproject.entity.CitizenBasicI import fr.dcproject.entity.CitizenBasicI
import fr.dcproject.entity.UserI import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.user import fr.dcproject.user
import fr.ktorVoter.ActionI import fr.dcproject.voter.NoRuleDefinedException
import fr.ktorVoter.Vote import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.Voter import fr.ktorVoter.*
import io.ktor.application.ApplicationCall import io.ktor.application.*
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.*
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
class CitizenVoter : Voter<ApplicationCall> { class CitizenVoter : Voter<ApplicationCall> {
@@ -19,45 +19,43 @@ class CitizenVoter : Voter<ApplicationCall> {
CHANGE_PASSWORD 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) if (!((action is Action)
&& (subject is CitizenBasicI?))) return Vote.ABSTAIN && (subject is CitizenBasicI?))) return abstain()
val user = context.user val user = context.user
if (action == Action.CREATE && user != null) { if (action == Action.CREATE && user != null) {
return Vote.GRANTED return granted()
} }
if (action == Action.VIEW) { 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) { if (subject is CitizenBasicI) {
return if (subject.isDeleted()) Vote.DENIED return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted")
else Vote.GRANTED else granted()
} }
return Vote.DENIED throw NoRuleDefinedException(action)
} }
if (action == Action.DELETE) { if (action == Action.DELETE) {
return Vote.DENIED return denied("You can never deleted a citizen", "citizen.delete.never")
} }
if (action == Action.UPDATE && if (action == Action.UPDATE) {
user is UserI && if (user == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected")
subject is CitizenBasicI && if (subject !is CitizenWithUserI) throw NoSubjectDefinedException(action)
subject.user.id == user.id return if (subject.user.id == user.id) granted() else denied("You can only update your citizen", "citizen.update.notYours")
) {
return Vote.GRANTED
} }
if (action == Action.CHANGE_PASSWORD && user != null && subject is CitizenBasicI) { if (action == Action.CHANGE_PASSWORD && user != null && subject is CitizenBasicI) {
val userToChange = subject.user val userToChange = subject.user
return if (user.id == userToChange.id) { return if (user.id == userToChange.id) {
Vote.GRANTED granted()
} else { } 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 package fr.dcproject.security.voter
import fr.dcproject.entity.Comment import fr.dcproject.citizenOrNull
import fr.dcproject.user import fr.dcproject.entity.CommentForUpdate
import fr.ktorVoter.ActionI import fr.dcproject.entity.CommentForView
import fr.ktorVoter.Vote import fr.dcproject.entity.CommentI
import fr.ktorVoter.Voter import fr.dcproject.voter.NoRuleDefinedException
import io.ktor.application.ApplicationCall import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
class CommentVoter : Voter<ApplicationCall> { class CommentVoter : Voter<ApplicationCall> {
enum class Action : ActionI { enum class Action : ActionI {
@@ -15,38 +18,47 @@ class CommentVoter : Voter<ApplicationCall> {
DELETE 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 Comment<*>?)) return Vote.ABSTAIN if (!(action is Action && subject is CommentI?)) return abstain()
val user = context.user val citizen = context.citizenOrNull
if (subject == null) { if (subject == null) {
return Vote.DENIED throw NoSubjectDefinedException(action)
} }
if (action == Action.CREATE) { if (action == Action.CREATE) {
if (user == null) { return when {
return Vote.DENIED 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) { if (action == Action.VIEW) {
return if (subject.isDeleted()) Vote.DENIED return when {
else Vote.GRANTED 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) { if (action == Action.UPDATE) {
return Vote.GRANTED 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) { 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 package fr.dcproject.security.voter
import fr.dcproject.entity.Comment import fr.dcproject.entity.CommentForView
import fr.dcproject.entity.ConstitutionSimple import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.entity.UserI import fr.dcproject.entity.UserI
import fr.dcproject.user import fr.dcproject.user
import fr.ktorVoter.ActionI import fr.dcproject.voter.NoRuleDefinedException
import fr.ktorVoter.Vote import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.Voter import fr.ktorVoter.*
import io.ktor.application.ApplicationCall import io.ktor.application.*
import fr.dcproject.entity.Vote as VoteEntity import fr.dcproject.entity.Vote as VoteEntity
class ConstitutionVoter : Voter<ApplicationCall> { class ConstitutionVoter : Voter<ApplicationCall> {
@@ -18,63 +18,63 @@ class ConstitutionVoter : Voter<ApplicationCall> {
DELETE 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) 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 val user = context.user
if (action == Action.CREATE && user != null) { if (action == Action.CREATE && user != null) {
return Vote.GRANTED return granted()
} }
if (action == Action.VIEW) { if (action == Action.VIEW) {
if (subject is ConstitutionSimple<*, *>) { if (subject is ConstitutionSimple<*, *>) {
return if (subject.isDeleted()) Vote.DENIED return if (subject.isDeleted()) denied("You cannot view a deleted constitution", "constitution.view.deleted")
else Vote.GRANTED 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) { 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) { 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 CommentVoter.Action) return voteForComment(action)
if (action is VoteVoter.Action) return voteForVote(action, subject) if (action is VoteVoter.Action) return voteForVote(action, subject)
if (action is Action) { 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<*>) { if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
val target = subject.target val target = subject.target
if (target !is ConstitutionSimple<*, *>) { if (target !is ConstitutionSimple<*, *>) {
return Vote.ABSTAIN return abstain()
} }
if (target.isDeleted()) { 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) { if (action == CommentVoter.Action.CREATE) {
return Vote.GRANTED return granted()
} }
if (action == CommentVoter.Action.VIEW) { 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 package fr.dcproject.security.voter
import fr.dcproject.user import fr.dcproject.citizenOrNull
import fr.ktorVoter.ActionI import fr.dcproject.entity.CitizenI
import fr.ktorVoter.Vote import fr.dcproject.entity.FollowI
import fr.ktorVoter.Voter import fr.dcproject.voter.NoSubjectDefinedException
import io.ktor.application.ApplicationCall import fr.ktorVoter.*
import io.ktor.application.*
import fr.dcproject.entity.Follow as FollowEntity import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.entity.User as UserEntity
class FollowVoter : Voter<ApplicationCall> { class FollowVoter : Voter<ApplicationCall> {
enum class Action : ActionI { enum class Action : ActionI {
@@ -15,33 +15,33 @@ class FollowVoter : Voter<ApplicationCall> {
VIEW 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) if (action !is Action) return abstain()
&& (subject is FollowEntity<*>?))) return Vote.ABSTAIN if (subject !is FollowI) throw NoSubjectDefinedException(action)
val user = context.user val citizen = context.citizenOrNull
if (action == Action.CREATE) { if (action == Action.CREATE) {
return if (user != null) Vote.GRANTED return if (citizen == null) denied("You must be connected to follow", "follow.create.notConnected")
else Vote.DENIED else granted()
} }
if (action == Action.DELETE) { if (action == Action.DELETE) {
return if (user != null) Vote.GRANTED return if (citizen == null) denied("You must be connected to unfollow", "follow.delete.notConnected")
else Vote.DENIED else granted()
} }
if (action == Action.VIEW) { if (action == Action.VIEW) {
if (subject is FollowEntity<*>) { 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 { private fun voteView(citizen: CitizenI?, subject: FollowEntity<*>): VoterResponseI {
return if ((user != null && subject.createdBy.user.id == user.id) || !subject.createdBy.followAnonymous) Vote.GRANTED return if ((citizen != null && subject.createdBy.id == citizen.id) || !subject.createdBy.followAnonymous) granted()
else Vote.DENIED 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 package fr.dcproject.security.voter
import fr.dcproject.entity.OpinionChoice import fr.dcproject.entity.OpinionChoice
import fr.ktorVoter.ActionI import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.Vote import fr.ktorVoter.*
import fr.ktorVoter.Voter import io.ktor.application.*
import io.ktor.application.ApplicationCall
class OpinionChoiceVoter : Voter<ApplicationCall> { class OpinionChoiceVoter : Voter<ApplicationCall> {
enum class Action : ActionI { enum class Action : ActionI {
VIEW 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) if (!((action is Action)
&& (subject is OpinionChoice?))) return Vote.ABSTAIN && (subject is OpinionChoice?))) return abstain()
if (action == Action.VIEW) { if (action == Action.VIEW) {
if (subject is OpinionChoice) { 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 package fr.dcproject.security.voter
import fr.dcproject.entity.Article import fr.dcproject.component.article.ArticleAuthI
import fr.dcproject.entity.ArticleAuthI import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.Opinion import fr.dcproject.entity.Opinion
import fr.dcproject.user import fr.dcproject.user
import fr.ktorVoter.ActionI import fr.dcproject.voter.NoRuleDefinedException
import fr.ktorVoter.Vote import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.Vote.Companion.toVote import fr.ktorVoter.*
import fr.ktorVoter.Voter import io.ktor.application.*
import io.ktor.application.ApplicationCall
class OpinionVoter : Voter<ApplicationCall> { class OpinionVoter : Voter<ApplicationCall> {
enum class Action : ActionI { enum class Action : ActionI {
@@ -17,32 +16,33 @@ class OpinionVoter : Voter<ApplicationCall> {
DELETE 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) 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 val user = context.user
if (action == Action.CREATE) { if (action == Action.CREATE) {
return toVote { if (user == null) return denied("You must be connected to make an opinion", "opinion.create.notConnected")
user != null && ( if (subject is ArticleAuthI<*> && !subject.isDeleted()) return granted()
(subject is ArticleAuthI<*> && !subject.isDeleted()) || if (subject is Opinion<*> && subject.createdBy.user.id == user.id) return granted()
(subject is Opinion<*> && subject.createdBy.user.id == user.id)
) throw NoSubjectDefinedException(action)
}
} }
if (action == Action.VIEW) { 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) { if (action == Action.DELETE) {
return toVote { if (user == null) return denied("You must be connected to delete opinion", "opinion.delete.notConnected")
subject is Opinion<*> && if (subject !is Opinion<*>) throw NoSubjectDefinedException(action)
user != null && return if (subject.createdBy.user.id == user.id) granted() else denied("You can only delete your opinions", "opinion.delete.notYours")
subject.createdBy.user.id == user.id
}
} }
return Vote.ABSTAIN if (action is Action) {
throw NoRuleDefinedException(action)
}
return abstain()
} }
} }

View File

@@ -1,10 +1,12 @@
package fr.dcproject.security.voter package fr.dcproject.security.voter
import fr.dcproject.user import fr.dcproject.citizenOrNull
import fr.ktorVoter.ActionI import fr.dcproject.entity.VoteForUpdateI
import fr.ktorVoter.Vote import fr.dcproject.entity.VoteI
import fr.ktorVoter.Voter import fr.dcproject.voter.NoSubjectDefinedException
import io.ktor.application.ApplicationCall import fr.ktorVoter.*
import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
import fr.dcproject.entity.Vote as VoteEntity import fr.dcproject.entity.Vote as VoteEntity
class VoteVoter : Voter<ApplicationCall> { class VoteVoter : Voter<ApplicationCall> {
@@ -13,26 +15,36 @@ class VoteVoter : Voter<ApplicationCall> {
VIEW 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 VoteEntity<*>?)) return Vote.ABSTAIN 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) { 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 (action == Action.VIEW) {
if (subject is VoteEntity<*>) { if (subject is VoteEntity<*>) {
return if (subject.createdBy.user.id != user.id) { return if (subject.createdBy.id != citizen.id) {
Vote.DENIED denied("You can view only your votes", "vote.view")
} else { } 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 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.entity.WorkgroupWithMembersI.Member.Role
import fr.dcproject.user import fr.dcproject.user
import fr.ktorVoter.ActionI import fr.dcproject.voter.NoRuleDefinedException
import fr.ktorVoter.Vote import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.Voter import fr.ktorVoter.*
import fr.ktorVoter.VoterException import io.ktor.application.*
import io.ktor.application.ApplicationCall
class WorkgroupVoter : Voter<ApplicationCall> { class WorkgroupVoter : Voter<ApplicationCall> {
enum class Action : ActionI { enum class Action : ActionI {
@@ -24,67 +25,72 @@ class WorkgroupVoter : Voter<ApplicationCall> {
REMOVE, 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) 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 val user = context.user
if (subject is WorkgroupI && action == Action.CREATE && user is UserI) { if (action == Action.CREATE) {
return Vote.GRANTED 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 (action == Action.VIEW) {
if (subject is WorkgroupWithAuthI<*>) { if (subject is WorkgroupWithAuthI<*>) {
return if (subject.isDeleted()) Vote.DENIED return if (subject.isDeleted()) denied("You cannot view a deleted workgroup", "workgroup.view.deleted")
else if (!subject.anonymous) Vote.GRANTED else if (!subject.anonymous) granted()
else if (subject.anonymous && user != null && subject.isMember(user)) Vote.GRANTED else if (subject.anonymous && user != null && subject.isMember(user)) granted()
else Vote.DENIED else denied("You cannot view anonymous workgroup", "workgroup.view.anonymous")
} }
return Vote.DENIED throw NoSubjectDefinedException(action as ActionI)
} }
if (subject is WorkgroupWithAuthI<*>) { if (subject is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
if (action == Action.DELETE && user is UserI && subject.hasRole(Role.MASTER, user)) { if (action == Action.DELETE) {
return Vote.GRANTED 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)) { throw NoRuleDefinedException(action as ActionI)
return Vote.GRANTED
}
return Vote.DENIED
} else if (subject !is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) { } else if (subject !is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
throw object : throw NoSubjectDefinedException(action as ActionI)
VoterException("Unable to define if your are granted, the subject must implement 'WorkgroupWithAuthI'") {}
} }
if (action == ActionMembers.ADD) { if (action == ActionMembers.ADD) {
// TODO create ROLES // TODO create ROLES
return Vote.toVote { if (user !is UserI) return denied("You must be connected to add member to the workgroup", "workgroup.addMember.notConnected")
user is UserI && if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
subject is WorkgroupWithAuthI<*> && return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for add member to workgroup", "workgroup.addMember.role")
subject.hasRole(Role.MASTER, user)
}
} }
if (action == ActionMembers.UPDATE) { if (action == ActionMembers.UPDATE) {
// TODO create ROLES // TODO create ROLES
return Vote.toVote { if (user !is UserI) return denied("You must be connected to update member of the workgroup", "workgroup.updateMember.notConnected")
user is UserI && if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
subject is WorkgroupWithAuthI<*> && return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for update members of workgroup", "workgroup.updateMember.role")
subject.hasRole(Role.MASTER, user)
}
} }
if (action == ActionMembers.REMOVE) { if (action == ActionMembers.REMOVE) {
// TODO create ROLES // TODO create ROLES
return Vote.toVote { if (user !is UserI) return denied("You must be connected to remove member of the workgroup", "workgroup.removeMember.notConnected")
user is UserI && if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
subject is WorkgroupWithAuthI<*> && return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for remove members of workgroup", "workgroup.removeMember.role")
subject.hasRole(Role.MASTER, user)
}
} }
return Vote.ABSTAIN if (action is Action) {
throw NoRuleDefinedException(action)
}
return abstain()
} }
} }

View File

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

View File

@@ -6,7 +6,16 @@ begin
select to_json(t) select to_json(t)
from ( from (
select 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_citizen_by_id_with_user(a.created_by_id) as created_by,
find_workgroup_by_id(a.workgroup_id) as workgroup, find_workgroup_by_id(a.workgroup_id) as workgroup,
count_vote(a.id) as votes, count_vote(a.id) as votes,

View File

@@ -21,11 +21,13 @@ begin
into resource, total into resource, total
from ( from (
select 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_citizen_by_id_with_user(a.created_by_id) as created_by,
find_workgroup_by_id(a.workgroup_id) as workgroup, find_workgroup_by_id(a.workgroup_id) as workgroup,
count_vote(a.id) as votes, count_vote(a.id) as votes,
count_opinion(a.id) as opinions,
zdb.score(a.ctid) _score zdb.score(a.ctid) _score
from article as a from article as a
left join vote_cache ca using (id) 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) insert into article_relations (source_id, target_id, created_by_id)
select select
(resource->>'id')::uuid, (rel->>'id')::uuid,
id, id,
(resource#>>'{created_by, id}')::uuid (rel#>>'{created_by, id}')::uuid
from json_populate_recordset(null::article, resource->>'relations'); from json_populate_recordset(null::article, resource->>'relations') rel;
end if; end if;
select find_article_by_id(new_id) into resource; select find_article_by_id(new_id) into resource;

View File

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

View File

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

View File

@@ -9,6 +9,8 @@ begin
from ( from (
select select
com.*, 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_reference_by_id(com.target_id, com.target_reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by, find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes 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 ( from (
select select
com.*, 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_reference_by_id(com.target_id, _reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by, find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes count_vote(com.id) as votes

View File

@@ -13,6 +13,7 @@ begin
select select
com.*, com.*,
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count, (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_reference_by_id(com.target_id, com.target_reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by, find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes count_vote(com.id) as votes

View File

@@ -14,6 +14,7 @@ begin
select select
com.*, com.*,
(select count(c2) from "comment" c2 where c2.parent_comment_id = com.id) as children_count, (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_reference_by_id(com.target_id, com.target_reference) as target,
find_citizen_by_id_with_user(com.created_by_id) as created_by, find_citizen_by_id_with_user(com.created_by_id) as created_by,
count_vote(com.id) as votes 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 language plpgsql as
$$ $$
declare _anonymous_conf bool = coalesce(_anonymous, (select vote_anonymous from citizen where id = _created_by_id));
begin begin
if reference = 'article'::regclass then if reference = 'article'::regclass then
insert into vote_for_article (created_by_id, target_id, note, anonymous) 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 on conflict (created_by_id, target_id) do update set
note = excluded.note, note = excluded.note,
anonymous = excluded.anonymous, anonymous = excluded.anonymous,
updated_at = now(); updated_at = now();
elseif reference = 'constitution'::regclass then elseif reference = 'constitution'::regclass then
insert into vote_for_constitution (created_by_id, target_id, note, anonymous) 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 on conflict (created_by_id, target_id) do update set
note = excluded.note, note = excluded.note,
anonymous = excluded.anonymous, anonymous = excluded.anonymous,
updated_at = now(); updated_at = now();
elseif reference = 'comment_on_article'::regclass then elseif reference = 'comment_on_article'::regclass then
insert into vote_for_comment_on_article (created_by_id, target_id, note, anonymous) 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 on conflict (created_by_id, target_id) do update set
note = excluded.note, note = excluded.note,
anonymous = excluded.anonymous, anonymous = excluded.anonymous,
updated_at = now(); updated_at = now();
elseif reference = 'comment_on_constitution'::regclass then elseif reference = 'comment_on_constitution'::regclass then
insert into vote_for_comment_on_constitution (created_by_id, target_id, note, anonymous) 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 on conflict (created_by_id, target_id) do update set
note = excluded.note, note = excluded.note,
anonymous = excluded.anonymous, anonymous = excluded.anonymous,

View File

@@ -23,9 +23,9 @@ begin
insert into citizen_in_workgroup (workgroup_id, citizen_id, roles) insert into citizen_in_workgroup (workgroup_id, citizen_id, roles)
select select
new_id::uuid, new_id::uuid,
citizen_id, (m#>>'{citizen,id}')::uuid,
roles json_to_array(m#>'{roles}')
from json_populate_recordset(null::citizen_in_workgroup, resource->'members') m; from json_array_elements(resource->'members') m;
-- insert master if no members -- insert master if no members
if (exists) then 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_citizen;
drop table if exists follow; drop table if exists follow;
drop table if exists vote_cache;
drop table if exists vote_for_article; drop table if exists vote_for_article;
drop table if exists vote_for_constitution; drop table if exists vote_for_constitution;
drop table if exists vote_for_comment_on_article; 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