Refactors Articles and Voter
- Move files into components (article) - Split articles routes - Refactoring for remove ktor-voter (ArticleVoter) - Remove mutability - Move DataConversion to separate file (Converter.kt) - Add Schemas for Articles routes - Fix SQL Query for Workgroup roles - rename container_name in docker-compose
This commit is contained in:
2
.env
2
.env
@@ -1,4 +1,4 @@
|
|||||||
NAME=dc-project
|
APP_NAME=dc-project
|
||||||
|
|
||||||
DATABASE_URL=jdbc:postgresql:dc-project
|
DATABASE_URL=jdbc:postgresql:dc-project
|
||||||
|
|
||||||
|
|||||||
22
.idea/codeStyles/Project.xml
generated
22
.idea/codeStyles/Project.xml
generated
@@ -2,28 +2,10 @@
|
|||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<option name="LINE_SEPARATOR" value=" " />
|
<option name="LINE_SEPARATOR" value=" " />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -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="" />
|
||||||
|
|||||||
6
.idea/runConfigurations/Voter_Tests.xml
generated
6
.idea/runConfigurations/Voter_Tests.xml
generated
@@ -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="" />
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -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}
|
||||||
|
|||||||
@@ -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
98
doc/schema/Article.puml
Normal 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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}\"")
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/main/kotlin/Converters.kt
Normal file
118
src/main/kotlin/Converters.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
126
src/main/kotlin/component/article/Article.kt
Normal file
126
src/main/kotlin/component/article/Article.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
49
src/main/kotlin/component/article/ArticleVoter.kt
Normal file
49
src/main/kotlin/component/article/ArticleVoter.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/kotlin/component/article/routes/FindArticles.kt
Normal file
46
src/main/kotlin/component/article/routes/FindArticles.kt
Normal 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/main/kotlin/component/article/routes/GetOneArticle.kt
Normal file
69
src/main/kotlin/component/article/routes/GetOneArticle.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/main/kotlin/component/article/routes/UpsertArticle.kt
Normal file
66
src/main/kotlin/component/article/routes/UpsertArticle.kt
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/kotlin/dto/CreatedAt.kt
Normal file
12
src/main/kotlin/dto/CreatedAt.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/kotlin/dto/Opinionable.kt
Normal file
11
src/main/kotlin/dto/Opinionable.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/kotlin/dto/Versionable.kt
Normal file
15
src/main/kotlin/dto/Versionable.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
10
src/main/kotlin/dto/ViewAggregation.kt
Normal file
10
src/main/kotlin/dto/ViewAggregation.kt
Normal 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)
|
||||||
|
}
|
||||||
9
src/main/kotlin/dto/Viewable.kt
Normal file
9
src/main/kotlin/dto/Viewable.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/kotlin/dto/Votable.kt
Normal file
9
src/main/kotlin/dto/Votable.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/kotlin/dto/VoteAggregation.kt
Normal file
11
src/main/kotlin/dto/VoteAggregation.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
13
src/main/kotlin/entity/CreatedBy.kt
Normal file
13
src/main/kotlin/entity/CreatedBy.kt
Normal 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>
|
||||||
8
src/main/kotlin/entity/EntityI.kt
Normal file
8
src/main/kotlin/entity/EntityI.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package fr.dcproject.entity
|
||||||
|
|
||||||
|
import fr.postgresjson.entity.EntityI
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface EntityI : EntityI {
|
||||||
|
val id: UUID
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
|
|||||||
17
src/main/kotlin/entity/Versionable.kt
Normal file
17
src/main/kotlin/entity/Versionable.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package fr.dcproject.entity
|
|
||||||
|
|
||||||
interface Viewable {
|
|
||||||
var views: ViewAggregation
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewableImp : Viewable {
|
|
||||||
override var views: ViewAggregation = ViewAggregation()
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)!!
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/main/kotlin/voter/NoRuleDefinedException.kt
Normal file
6
src/main/kotlin/voter/NoRuleDefinedException.kt
Normal 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""")
|
||||||
6
src/main/kotlin/voter/NoSubjectDefinedException.kt
Normal file
6
src/main/kotlin/voter/NoSubjectDefinedException.kt
Normal 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""")
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/main/kotlin/voter/VoterModule.kt
Normal file
87
src/main/kotlin/voter/VoterModule.kt
Normal 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>
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
$$;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user