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:
@@ -8,36 +8,33 @@ import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
|
||||
import fr.dcproject.Env.PROD
|
||||
import fr.dcproject.component.article.route.findArticleVersions
|
||||
import fr.dcproject.component.article.route.upsertArticle
|
||||
import fr.dcproject.component.article.routes.findArticles
|
||||
import fr.dcproject.component.article.routes.getOneArticle
|
||||
import fr.dcproject.elasticsearch.configElasticIndexes
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.entity.User
|
||||
import fr.dcproject.event.EventNotification
|
||||
import fr.dcproject.event.EventSubscriber
|
||||
import fr.dcproject.routes.*
|
||||
import fr.dcproject.security.voter.*
|
||||
import fr.ktorVoter.AuthorizationVoter
|
||||
import fr.ktorVoter.ForbiddenException
|
||||
import fr.ktorVoter.VoterException
|
||||
import fr.postgresjson.migration.Migrations
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.application.install
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.auth.jwt.jwt
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.jetty.Jetty
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.auth.jwt.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.jetty.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.auth.HttpAuthHeader
|
||||
import io.ktor.jackson.jackson
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Locations
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Routing
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import io.ktor.websocket.WebSockets
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.auth.*
|
||||
import io.ktor.jackson.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.util.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import org.eclipse.jetty.util.log.Slf4jLog
|
||||
import org.koin.core.qualifier.named
|
||||
@@ -47,13 +44,7 @@ import org.slf4j.event.Level
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletionException
|
||||
import fr.dcproject.entity.Workgroup as WorkgroupEntity
|
||||
import fr.dcproject.repository.Article as RepositoryArticle
|
||||
import fr.dcproject.repository.Citizen as RepositoryCitizen
|
||||
import fr.dcproject.repository.Constitution as RepositoryConstitution
|
||||
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
|
||||
import fr.dcproject.repository.User as UserRepository
|
||||
import fr.dcproject.repository.Workgroup as WorkgroupRepository
|
||||
|
||||
fun main(args: Array<String>): Unit = io.ktor.server.jetty.EngineMain.main(args)
|
||||
|
||||
@@ -66,117 +57,19 @@ enum class Env { PROD, TEST, CUCUMBER }
|
||||
fun Application.module(env: Env = PROD) {
|
||||
install(Koin) {
|
||||
Slf4jLog()
|
||||
modules(Module)
|
||||
modules(KoinModule)
|
||||
}
|
||||
|
||||
install(CallLogging) {
|
||||
level = Level.INFO
|
||||
}
|
||||
|
||||
install(DataConversion) {
|
||||
convert<UUID> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
}
|
||||
install(DataConversion, converters)
|
||||
|
||||
encode { value ->
|
||||
when (value) {
|
||||
null -> listOf()
|
||||
is UUID -> listOf(value.toString())
|
||||
else -> throw InternalError("Cannot convert $value as UUID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create generic convert for entityI
|
||||
convert<Article> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
get<RepositoryArticle>().findById(UUID.fromString(it))
|
||||
?: throw NotFoundException("Article $values not found")
|
||||
} ?: throw NotFoundException("Article $values not found")
|
||||
}
|
||||
}
|
||||
convert<ArticleRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
ArticleRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Article""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<CommentRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
CommentRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Comment""")
|
||||
}
|
||||
}
|
||||
convert<ConstitutionRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
ConstitutionRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Constitution""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<Constitution> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<RepositoryConstitution>().findById(id) ?: throw NotFoundException("Constitution $values not found")
|
||||
}
|
||||
}
|
||||
|
||||
convert<Citizen> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<RepositoryCitizen>().findById(id) ?: throw NotFoundException("Citizen $values not found")
|
||||
}
|
||||
}
|
||||
|
||||
convert<CitizenRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
CitizenRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Citizen""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<OpinionChoice> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<OpinionChoiceRepository>().findOpinionChoiceById(id)
|
||||
?: throw NotFoundException("OpinionChoice $values not found")
|
||||
}
|
||||
}
|
||||
|
||||
convert<WorkgroupRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
WorkgroupRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Workgroup""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<WorkgroupEntity> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<WorkgroupRepository>().findById(id)
|
||||
?: throw NotFoundException("Workgroup $values not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(Locations) {
|
||||
}
|
||||
install(Locations)
|
||||
|
||||
install(AuthorizationVoter) {
|
||||
voters = listOf(
|
||||
ArticleVoter(get()),
|
||||
ConstitutionVoter(),
|
||||
CitizenVoter(),
|
||||
CommentVoter(),
|
||||
@@ -255,10 +148,13 @@ fun Application.module(env: Env = PROD) {
|
||||
}
|
||||
}
|
||||
|
||||
install(Routing) {
|
||||
install(Routing.Feature) {
|
||||
// trace { application.log.trace(it.buildText()) }
|
||||
authenticate(optional = true) {
|
||||
article(get(), get())
|
||||
findArticles(get(), get())
|
||||
getOneArticle(get(), get())
|
||||
upsertArticle(get(), get(), get())
|
||||
findArticleVersions(get(), get())
|
||||
auth(get(), get(), get())
|
||||
citizen(get(), get())
|
||||
constitution(get())
|
||||
@@ -293,6 +189,10 @@ fun Application.module(env: Env = PROD) {
|
||||
exception<NotFoundException> { e ->
|
||||
call.respond(HttpStatusCode.NotFound, e.message!!)
|
||||
}
|
||||
exception<VoterException> {
|
||||
if (call.user == null) call.respond(HttpStatusCode.Unauthorized)
|
||||
else call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
exception<ForbiddenException> {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
|
||||
@@ -2,20 +2,21 @@ package fr.dcproject
|
||||
|
||||
import fr.dcproject.entity.User
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.ktorVoter.ForbiddenException
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.auth.authentication
|
||||
import io.ktor.util.AttributeKey
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.util.*
|
||||
import io.ktor.util.pipeline.*
|
||||
import org.koin.core.context.GlobalContext
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
|
||||
class ForbiddenException(message: String) : Exception(message)
|
||||
|
||||
private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
|
||||
|
||||
val ApplicationCall.citizen: CitizenEntity
|
||||
get() = attributes.computeIfAbsent(citizenAttributeKey) {
|
||||
val user = authentication.principal<UserI>() ?: throw ForbiddenException()
|
||||
val user = authentication.principal<UserI>() ?: throw ForbiddenException("No User Connected")
|
||||
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
|
||||
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
|
||||
}
|
||||
|
||||
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.module.kotlin.jacksonObjectMapper
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import fr.dcproject.component.article.ArticleRepository
|
||||
import fr.dcproject.component.article.ArticleViewManager
|
||||
import fr.dcproject.component.article.ArticleVoter
|
||||
import fr.dcproject.event.publisher.Publisher
|
||||
import fr.dcproject.messages.Mailer
|
||||
import fr.dcproject.messages.NotificationEmailSender
|
||||
import fr.dcproject.messages.SsoManager
|
||||
import fr.dcproject.views.ArticleViewManager
|
||||
import fr.postgresjson.connexion.Connection
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.migration.Migrations
|
||||
@@ -25,7 +27,6 @@ import org.apache.http.HttpHost
|
||||
import org.elasticsearch.client.RestClient
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import fr.dcproject.repository.Article as ArticleRepository
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
|
||||
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
|
||||
@@ -42,7 +43,7 @@ import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
|
||||
import fr.dcproject.repository.Workgroup as WorkgroupRepository
|
||||
|
||||
@KtorExperimentalAPI
|
||||
val Module = module {
|
||||
val KoinModule = module {
|
||||
|
||||
single { Config }
|
||||
|
||||
@@ -114,6 +115,9 @@ val Module = module {
|
||||
single { OpinionArticleRepository(get()) }
|
||||
single { WorkgroupRepository(get()) }
|
||||
|
||||
// Voters
|
||||
single { ArticleVoter(get()) }
|
||||
|
||||
// Elasticsearch Client
|
||||
single<RestClient> {
|
||||
RestClient.builder(
|
||||
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.Requester
|
||||
import fr.postgresjson.entity.Parameter
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import fr.postgresjson.repository.RepositoryI.Direction
|
||||
import net.pearx.kasechange.toSnakeCase
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
|
||||
class Article(override var requester: Requester) : RepositoryI {
|
||||
fun findById(id: UUID): ArticleEntity? {
|
||||
class ArticleRepository(override var requester: Requester) : RepositoryI {
|
||||
fun findById(id: UUID): ArticleForView? {
|
||||
val function = requester.getFunction("find_article_by_id")
|
||||
return function.selectOne("id" to id)
|
||||
}
|
||||
|
||||
fun findVerionsByVersionsId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleEntity> {
|
||||
fun findVersionsByVersionId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleForView> {
|
||||
return requester
|
||||
.getFunction("find_articles_versions_by_version_id")
|
||||
.select(page, limit, "version_id" to versionId)
|
||||
@@ -27,10 +23,10 @@ class Article(override var requester: Requester) : RepositoryI {
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: String? = null,
|
||||
direction: Direction? = null,
|
||||
direction: RepositoryI.Direction? = null,
|
||||
search: String? = null,
|
||||
filter: Filter = Filter()
|
||||
): Paginated<ArticleSimple> {
|
||||
): Paginated<ArticleForListing> {
|
||||
return requester
|
||||
.getFunction("find_articles")
|
||||
.select(
|
||||
@@ -42,7 +38,7 @@ class Article(override var requester: Requester) : RepositoryI {
|
||||
)
|
||||
}
|
||||
|
||||
fun upsert(article: ArticleForUpdate): ArticleEntity? {
|
||||
fun upsert(article: ArticleForUpdate): ArticleForView? {
|
||||
return requester
|
||||
.getFunction("upsert_article")
|
||||
.selectOne("resource" to article)
|
||||
@@ -52,4 +48,4 @@ class Article(override var requester: Requester) : RepositoryI {
|
||||
val createdById: String? = null,
|
||||
val workgroupId: String? = null
|
||||
) : Parameter
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
package fr.dcproject.views
|
||||
package fr.dcproject.component.article
|
||||
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.entity.CitizenI
|
||||
import fr.dcproject.entity.ViewAggregation
|
||||
import fr.dcproject.utils.contentToString
|
||||
import fr.dcproject.utils.getJsonField
|
||||
import fr.dcproject.utils.toIso
|
||||
import fr.dcproject.views.ViewManager
|
||||
import org.elasticsearch.client.Request
|
||||
import org.elasticsearch.client.Response
|
||||
import org.elasticsearch.client.RestClient
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class ArticleViewManager(private val restClient: RestClient) : ViewManager<ArticleRefVersioning> {
|
||||
override fun addView(ip: String, article: ArticleRefVersioning, citizen: CitizenRef?, dateTime: DateTime): Response? {
|
||||
/**
|
||||
* Wrapper for manage views with elasticsearch
|
||||
*/
|
||||
class ArticleViewManager(private val restClient: RestClient) : ViewManager<ArticleRefVersioningI> {
|
||||
/**
|
||||
* Add view on article to elasticsearch
|
||||
*/
|
||||
override fun addView(ip: String, article: ArticleRefVersioningI, citizen: CitizenI?, dateTime: DateTime): Response? {
|
||||
val isLogged = (citizen != null).toString()
|
||||
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
|
||||
val request = Request(
|
||||
@@ -36,7 +44,10 @@ class ArticleViewManager(private val restClient: RestClient) : ViewManager<Artic
|
||||
return restClient.performRequest(request)
|
||||
}
|
||||
|
||||
override fun getViewsCount(article: ArticleRefVersioning): ViewAggregation {
|
||||
/**
|
||||
* Get article views aggregations from elasticsearch
|
||||
*/
|
||||
override fun getViewsCount(article: ArticleRefVersioningI): ViewAggregation {
|
||||
val request = Request(
|
||||
"GET",
|
||||
"/views/_search"
|
||||
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) {
|
||||
val logger: Logger = LoggerFactory.getLogger("fr.dcproject.elasticsearch")
|
||||
val request = Request("GET", "/_cluster/health")
|
||||
repeat(40) {
|
||||
repeat(5*60/2) { // 5 minutes
|
||||
runCatching {
|
||||
client.performRequest(request).statusLine.statusCode
|
||||
}.onSuccess {
|
||||
|
||||
@@ -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
|
||||
|
||||
import fr.dcproject.entity.CitizenI.Name
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import fr.postgresjson.entity.*
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
class Citizen(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
name: Name,
|
||||
email: String,
|
||||
birthday: DateTime,
|
||||
voteAnonymous: Boolean = true,
|
||||
followAnonymous: Boolean = true,
|
||||
override val user: User
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
override val name: Name,
|
||||
override val email: String,
|
||||
override val birthday: DateTime,
|
||||
override val voteAnonymous: Boolean = true,
|
||||
override val followAnonymous: Boolean = true,
|
||||
override val user: User,
|
||||
deletedAt: DateTime? = null
|
||||
) : CitizenFull,
|
||||
CitizenBasic(id, name, email, birthday, voteAnonymous, followAnonymous, user),
|
||||
EntityCreatedAt by EntityCreatedAtImp() {
|
||||
CitizenBasicI,
|
||||
CitizenRef(id),
|
||||
CitizenCartI,
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityDeletedAt by EntityDeletedAtImp(deletedAt) {
|
||||
var workgroups: List<WorkgroupAndRoles> = emptyList()
|
||||
|
||||
class WorkgroupAndRoles(
|
||||
@@ -29,29 +29,43 @@ class Citizen(
|
||||
)
|
||||
}
|
||||
|
||||
open class CitizenBasic(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
name: Name,
|
||||
@Deprecated("")
|
||||
data class CitizenBasic(
|
||||
override var id: UUID = UUID.randomUUID(),
|
||||
override var name: Name,
|
||||
override var email: String,
|
||||
override var birthday: DateTime,
|
||||
override var voteAnonymous: Boolean = true,
|
||||
override var followAnonymous: Boolean = true,
|
||||
override val user: User
|
||||
override val user: User,
|
||||
override val deletedAt: DateTime? = null
|
||||
) : CitizenBasicI,
|
||||
CitizenSimple(id, name, user)
|
||||
CitizenRefWithUser(id, user),
|
||||
EntityDeletedAt by EntityDeletedAtImp(deletedAt)
|
||||
|
||||
@Deprecated("")
|
||||
open class CitizenSimple(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
var name: Name,
|
||||
user: UserRef
|
||||
) : CitizenRefWithUser(id, user)
|
||||
|
||||
class CitizenCart(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val name: Name,
|
||||
override val user: UserRef
|
||||
) : CitizenRef(id),
|
||||
CitizenCartI
|
||||
|
||||
interface CitizenCartI : CitizenI, CitizenWithUserI {
|
||||
val name: Name
|
||||
}
|
||||
|
||||
open class CitizenRefWithUser(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val user: UserRef
|
||||
) : CitizenWithUserI,
|
||||
CitizenRef(id),
|
||||
EntityDeletedAt by EntityDeletedAtImp()
|
||||
CitizenRef(id)
|
||||
|
||||
open class CitizenRef(
|
||||
id: UUID = UUID.randomUUID()
|
||||
@@ -60,22 +74,29 @@ open class CitizenRef(
|
||||
|
||||
interface CitizenI : UuidEntityI {
|
||||
data class Name(
|
||||
var firstName: String,
|
||||
var lastName: String,
|
||||
var civility: String? = null
|
||||
) {
|
||||
override val firstName: String,
|
||||
override val lastName: String,
|
||||
override val civility: String? = null
|
||||
) : NameI
|
||||
|
||||
interface NameI {
|
||||
val firstName: String
|
||||
val lastName: String
|
||||
val civility: String?
|
||||
fun getFullName(): String = "${civility ?: ""} $firstName $lastName".trim()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt {
|
||||
var name: Name
|
||||
var email: String
|
||||
var birthday: DateTime
|
||||
var voteAnonymous: Boolean
|
||||
var followAnonymous: Boolean
|
||||
val name: Name
|
||||
val email: String
|
||||
val birthday: DateTime
|
||||
val voteAnonymous: Boolean
|
||||
val followAnonymous: Boolean
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
interface CitizenFull : CitizenBasicI {
|
||||
override val user: User
|
||||
}
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import fr.postgresjson.entity.*
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
open class Comment<T : TargetI>(
|
||||
class CommentForView<T : TargetI, C : CitizenRef>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
override var target: T,
|
||||
var content: String,
|
||||
val responses: List<Comment<T>>? = null,
|
||||
var parent: Comment<T>? = null,
|
||||
val parentsIds: List<UUID>? = null,
|
||||
val childrenCount: Int? = null
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
CommentRef(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
|
||||
override val createdBy: C,
|
||||
override val target: T,
|
||||
override var content: String,
|
||||
override val parent: CommentParent<T>? = null,
|
||||
val childrenCount: Int? = null,
|
||||
override val deletedAt: DateTime? = null
|
||||
) : ExtraI<T, C>,
|
||||
CommentForUpdate<T, C>(id, createdBy, target, content, parent, deletedAt),
|
||||
CommentWithTargetI<T>,
|
||||
EntityCreatedBy<C> by EntityCreatedByImp(createdBy),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp(),
|
||||
EntityDeletedAt by EntityDeletedAtImp(),
|
||||
Votable by VotableImp(),
|
||||
TargetI {
|
||||
constructor(
|
||||
createdBy: CitizenBasic,
|
||||
parent: Comment<T>,
|
||||
createdBy: C,
|
||||
parent: CommentParent<T>,
|
||||
content: String
|
||||
) : this(
|
||||
createdBy = createdBy,
|
||||
@@ -34,6 +32,43 @@ open class Comment<T : TargetI>(
|
||||
)
|
||||
}
|
||||
|
||||
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id)
|
||||
open class CommentForUpdate<T : TargetI, C: CitizenRef>(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: C,
|
||||
override val target: T,
|
||||
open var content: String,
|
||||
open val parent: CommentParent<T>? = null,
|
||||
override val deletedAt: DateTime? = null
|
||||
) : CommentParent<T>(id, deletedAt, target),
|
||||
ExtraI<T, C>,
|
||||
CommentWithTargetI<T>,
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<C>,
|
||||
EntityDeletedAt,
|
||||
TargetI {
|
||||
constructor(
|
||||
createdBy: C,
|
||||
parent: CommentParent<T>,
|
||||
content: String
|
||||
) : this(
|
||||
createdBy = createdBy,
|
||||
parent = parent,
|
||||
target = parent.target,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
sealed class CommentS(id: UUID) : TargetRef(id)
|
||||
open class CommentParent<T: TargetI>(
|
||||
override val id: UUID,
|
||||
override val deletedAt: DateTime?,
|
||||
override val target: T
|
||||
) : CommentRef(id),
|
||||
CommentParentI<T>
|
||||
|
||||
interface CommentParentI<T: TargetI> : CommentI, EntityDeletedAt, CommentWithTargetI<T>
|
||||
|
||||
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T>
|
||||
|
||||
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
|
||||
|
||||
interface CommentI : EntityI
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import fr.postgresjson.entity.mutable.EntityVersioning
|
||||
import fr.postgresjson.entity.mutable.UuidEntityVersioning
|
||||
import fr.dcproject.component.article.ArticleI
|
||||
import fr.dcproject.component.article.ArticleSimple
|
||||
import fr.postgresjson.entity.*
|
||||
import java.util.*
|
||||
|
||||
class Constitution(
|
||||
@@ -33,17 +31,17 @@ class Constitution(
|
||||
) : ConstitutionSimple.TitleSimple<ArticleSimple>(id, name, rank)
|
||||
}
|
||||
|
||||
open class ConstitutionSimple<Cr : CitizenRefWithUser, T : ConstitutionSimple.TitleSimple<*>>(
|
||||
open class ConstitutionSimple<Cr : CitizenWithUserI, T : ConstitutionSimple.TitleSimple<*>>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
var title: String,
|
||||
var anonymous: Boolean = true,
|
||||
open var titles: MutableList<T> = mutableListOf(),
|
||||
var draft: Boolean = false,
|
||||
var lastVersion: Boolean = false,
|
||||
val title: String,
|
||||
val anonymous: Boolean = true,
|
||||
val titles: MutableList<T> = mutableListOf(),
|
||||
val draft: Boolean = false,
|
||||
val lastVersion: Boolean = false,
|
||||
override val createdBy: Cr,
|
||||
versionId: UUID = UUID.randomUUID()
|
||||
) : ConstitutionRef(id),
|
||||
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionId = versionId),
|
||||
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionId = versionId, versionNumber = 0),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<Cr> by EntityCreatedByImp(createdBy),
|
||||
EntityDeletedAt by EntityDeletedAtImp() {
|
||||
|
||||
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
|
||||
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedBy
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.postgresjson.entity.EntityCreatedAt
|
||||
import fr.postgresjson.entity.EntityCreatedBy
|
||||
import fr.postgresjson.entity.UuidEntity
|
||||
import fr.postgresjson.entity.UuidEntityI
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
|
||||
interface ExtraI<T : TargetI, C : CitizenI> :
|
||||
UuidEntityI,
|
||||
AsTarget<T>,
|
||||
EntityCreatedAt,
|
||||
EntityCreatedBy<C> {
|
||||
EntityCreatedBy<C>
|
||||
|
||||
interface AsTarget<T: TargetI> {
|
||||
val target: T
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.*
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
class Follow<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
@@ -10,11 +11,26 @@ class Follow<T : TargetI>(
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
FollowSimple<T, CitizenBasicI>(id, createdBy, target)
|
||||
|
||||
@Deprecated("")
|
||||
open class FollowSimple<T : TargetI, C : CitizenI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: C,
|
||||
override var target: T
|
||||
) : ExtraI<T, C>,
|
||||
UuidEntity(id),
|
||||
FollowRef(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<C> by EntityCreatedByImp(createdBy)
|
||||
|
||||
class FollowForUpdate<T: TargetI, C: CitizenI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val target: T,
|
||||
override val createdBy: C
|
||||
) : FollowRef(id),
|
||||
AsTarget<T>,
|
||||
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
|
||||
|
||||
open class FollowRef(
|
||||
override val id: UUID
|
||||
) : FollowI
|
||||
|
||||
interface FollowI: UuidEntityI
|
||||
@@ -1,11 +1,10 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedBy
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedByImp
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.postgresjson.entity.*
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
open class Opinion<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
@@ -19,9 +18,24 @@ open class Opinion<T : TargetI>(
|
||||
fun getName(): String = choice.name
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
class OpinionArticle(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
createdBy: CitizenBasic,
|
||||
target: ArticleRef,
|
||||
choice: OpinionChoice
|
||||
) : Opinion<ArticleRef>(id, createdBy, target, choice)
|
||||
) : Opinion<ArticleRef>(id, createdBy, target, choice)
|
||||
|
||||
data class OpinionForUpdate<T: TargetI>(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val target: T,
|
||||
val choice: OpinionChoice,
|
||||
override val createdBy: CitizenRef
|
||||
) : OpinionRef(id),
|
||||
EntityCreatedBy<CitizenI> by EntityCreatedByImp(createdBy)
|
||||
|
||||
open class OpinionRef(
|
||||
override val id: UUID
|
||||
) : OpinionI
|
||||
|
||||
interface OpinionI: UuidEntityI
|
||||
@@ -1,10 +1,6 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import fr.postgresjson.entity.*
|
||||
import java.util.*
|
||||
|
||||
class OpinionChoice(
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.EntityI
|
||||
|
||||
class OpinionAggregation(
|
||||
private val underlying: MutableMap<String, Any> = mutableMapOf()
|
||||
) : MutableMap<String, Any> by underlying, EntityI
|
||||
typealias Opinions = Map<String, Int>
|
||||
typealias OpinionsMutable = MutableMap<String, Int>
|
||||
|
||||
interface Opinionable {
|
||||
var opinions: MutableMap<String, Int>
|
||||
val opinions: Opinions
|
||||
}
|
||||
|
||||
class OpinionableImp : Opinionable {
|
||||
override var opinions: MutableMap<String, Int> = mutableMapOf()
|
||||
override var opinions: OpinionsMutable = mutableMapOf()
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.dcproject.entity.UserI.Roles
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import io.ktor.auth.Principal
|
||||
import fr.postgresjson.entity.*
|
||||
import io.ktor.auth.*
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
class User(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
username: String,
|
||||
@@ -16,6 +17,7 @@ class User(
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp()
|
||||
|
||||
@Deprecated("")
|
||||
open class UserBasic(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override var username: String,
|
||||
@@ -30,12 +32,19 @@ interface UserI : UuidEntityI, Principal {
|
||||
enum class Roles { ROLE_USER, ROLE_ADMIN }
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
interface UserBasicI : UserI {
|
||||
var username: String
|
||||
var blockedAt: DateTime?
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt {
|
||||
var plainPassword: String?
|
||||
var roles: List<Roles>
|
||||
}
|
||||
|
||||
interface UserForAuthI : UserI {
|
||||
var roles: List<Roles>
|
||||
var blockedAt: DateTime?
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import fr.postgresjson.entity.EntityI
|
||||
import fr.postgresjson.entity.immutable.EntityUpdatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityUpdatedAtImp
|
||||
import fr.postgresjson.entity.EntityUpdatedAt
|
||||
import fr.postgresjson.entity.EntityUpdatedAtImp
|
||||
|
||||
open class ViewAggregation(
|
||||
class ViewAggregation(
|
||||
val total: Int,
|
||||
val unique: Int
|
||||
) : EntityI,
|
||||
|
||||
@@ -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
|
||||
|
||||
interface Votable {
|
||||
var votes: VoteAggregation
|
||||
val votes: VoteAggregation
|
||||
}
|
||||
|
||||
class VotableImp : Votable {
|
||||
override var votes: VoteAggregation = VoteAggregation()
|
||||
override val votes: VoteAggregation = VoteAggregation()
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.*
|
||||
import java.util.*
|
||||
|
||||
open class Vote<T : TargetI>(
|
||||
@Deprecated("")
|
||||
class Vote<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
override var target: T,
|
||||
override val target: T,
|
||||
var note: Int,
|
||||
var anonymous: Boolean = true
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
UuidEntity(id),
|
||||
VoteRef(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp() {
|
||||
@@ -20,3 +20,26 @@ open class Vote<T : TargetI>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VoteForUpdate<T: TargetI, C: CitizenI>(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
override val note: Int,
|
||||
override val target: T,
|
||||
override val createdBy: C
|
||||
) : VoteRef(id),
|
||||
VoteForUpdateI<T, C>,
|
||||
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
|
||||
|
||||
interface VoteForUpdateI<T: TargetI, C: CitizenI> : VoteI, AsTarget<T>, EntityCreatedBy<C> {
|
||||
override val id: UUID
|
||||
val note: Int
|
||||
override val target: T
|
||||
override val createdBy: C
|
||||
}
|
||||
|
||||
|
||||
open class VoteRef(
|
||||
override val id: UUID
|
||||
) : VoteI
|
||||
|
||||
interface VoteI : UuidEntityI
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.EntityI
|
||||
import fr.postgresjson.entity.mutable.EntityUpdatedAt
|
||||
import fr.postgresjson.entity.mutable.EntityUpdatedAtImp
|
||||
import fr.postgresjson.entity.EntityUpdatedAt
|
||||
import fr.postgresjson.entity.EntityUpdatedAtImp
|
||||
|
||||
open class VoteAggregation(
|
||||
val up: Int,
|
||||
|
||||
@@ -2,22 +2,21 @@ package fr.dcproject.entity
|
||||
|
||||
import fr.dcproject.entity.WorkgroupWithMembersI.Member
|
||||
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
|
||||
import fr.postgresjson.entity.*
|
||||
import fr.postgresjson.entity.EntityI
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import java.util.*
|
||||
|
||||
class Workgroup(
|
||||
id: UUID? = null,
|
||||
name: String,
|
||||
description: String,
|
||||
logo: String? = null,
|
||||
anonymous: Boolean = true,
|
||||
createdBy: CitizenBasic,
|
||||
override var members: List<Member<CitizenBasic>> = emptyList()
|
||||
) : WorkgroupWithAuthI<CitizenBasic>,
|
||||
WorkgroupSimple<CitizenBasic>(
|
||||
@Deprecated("")
|
||||
data class Workgroup <C: CitizenBasicI>(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
override var name: String,
|
||||
override var description: String,
|
||||
override var logo: String? = null,
|
||||
override var anonymous: Boolean = true,
|
||||
override val createdBy: C,
|
||||
override var members: List<Member<C>> = emptyList()
|
||||
) : WorkgroupWithAuthI<C>,
|
||||
WorkgroupSimple<C>(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
@@ -28,17 +27,26 @@ class Workgroup(
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp()
|
||||
|
||||
open class WorkgroupSimple<Z : CitizenRef>(
|
||||
@Deprecated("")
|
||||
open class WorkgroupSimple<Z : CitizenI>(
|
||||
id: UUID? = null,
|
||||
var name: String,
|
||||
var description: String,
|
||||
var logo: String? = null,
|
||||
var anonymous: Boolean = true,
|
||||
open var name: String,
|
||||
open var description: String,
|
||||
open var logo: String? = null,
|
||||
open var anonymous: Boolean = true,
|
||||
createdBy: Z
|
||||
) : WorkgroupRef(id),
|
||||
EntityCreatedBy<Z> by EntityCreatedByImp(createdBy),
|
||||
EntityDeletedAt by EntityDeletedAtImp()
|
||||
|
||||
class WorkgroupCart(
|
||||
override val id: UUID,
|
||||
override val name: String
|
||||
) : WorkgroupCartI
|
||||
|
||||
interface WorkgroupCartI : UuidEntityI {
|
||||
val name: String
|
||||
}
|
||||
open class WorkgroupRef(
|
||||
id: UUID? = null
|
||||
) : UuidEntity(id ?: UUID.randomUUID()), WorkgroupI
|
||||
|
||||
@@ -3,7 +3,7 @@ package fr.dcproject.event
|
||||
import com.rabbitmq.client.*
|
||||
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
|
||||
import fr.dcproject.Config
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.FollowSimple
|
||||
import fr.dcproject.entity.TargetRef
|
||||
@@ -11,10 +11,8 @@ import fr.dcproject.event.publisher.Publisher
|
||||
import fr.dcproject.messages.NotificationEmailSender
|
||||
import fr.dcproject.repository.Follow
|
||||
import fr.postgresjson.serializer.deserialize
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.EventDefinition
|
||||
import io.ktor.application.application
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import io.ktor.application.*
|
||||
import io.ktor.util.pipeline.*
|
||||
import io.lettuce.core.api.async.RedisAsyncCommands
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
@@ -26,7 +24,7 @@ import org.slf4j.LoggerFactory
|
||||
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
|
||||
|
||||
class ArticleUpdate(
|
||||
target: Article
|
||||
target: ArticleForView
|
||||
) : EntityEvent(target, "article", "update") {
|
||||
companion object {
|
||||
val event = EventDefinition<ArticleUpdate>()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package fr.dcproject.event
|
||||
|
||||
import fr.postgresjson.entity.Serializable
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.UuidEntity
|
||||
import io.ktor.application.*
|
||||
import io.ktor.util.AttributeKey
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import io.ktor.util.*
|
||||
import kotlinx.coroutines.DisposableHandle
|
||||
import org.joda.time.DateTime
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
@@ -3,11 +3,15 @@ package fr.dcproject.messages
|
||||
import com.sendgrid.helpers.mail.Mail
|
||||
import com.sendgrid.helpers.mail.objects.Content
|
||||
import com.sendgrid.helpers.mail.objects.Email
|
||||
import fr.dcproject.entity.*
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import fr.dcproject.component.article.ArticleRepository
|
||||
import fr.dcproject.component.article.ArticleWithTitleI
|
||||
import fr.dcproject.entity.CitizenBasicI
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.FollowSimple
|
||||
import fr.dcproject.entity.TargetRef
|
||||
import fr.postgresjson.entity.UuidEntityI
|
||||
import java.util.*
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
import fr.dcproject.repository.Article as ArticleRepository
|
||||
|
||||
class NotificationEmailSender(
|
||||
private val mailer: Mailer,
|
||||
@@ -40,7 +44,7 @@ class NotificationEmailSender(
|
||||
|
||||
private fun generateHtmlContent(citizen: CitizenBasicI, target: UuidEntityI): String? {
|
||||
return when (target) {
|
||||
is Article -> """
|
||||
is ArticleWithTitleI -> """
|
||||
Hello ${citizen.name.getFullName()},<br/>
|
||||
The article "${target.title}" was updated, check it <a href="http://$domain/articles/${target.id}">here</a>
|
||||
""".trimIndent()
|
||||
@@ -50,7 +54,7 @@ class NotificationEmailSender(
|
||||
|
||||
private fun generateContent(citizen: CitizenBasicI, target: UuidEntityI): String {
|
||||
return when (target) {
|
||||
is Article -> """
|
||||
is ArticleWithTitleI -> """
|
||||
Hello ${citizen.name.getFullName()},
|
||||
The article "${target.title}" was updated, check it here: http://$domain/articles/${target.id}
|
||||
""".trimIndent()
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.entity.TargetI
|
||||
import fr.dcproject.entity.TargetRef
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.*
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import fr.postgresjson.entity.UuidEntityI
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
|
||||
abstract class Comment<T : TargetI>(override var requester: Requester) : RepositoryI {
|
||||
abstract fun findById(id: UUID): CommentEntity<T>?
|
||||
abstract fun findById(id: UUID): CommentForView<T, CitizenRef>?
|
||||
|
||||
abstract fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
citizen: CitizenI,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<CommentEntity<T>>
|
||||
): Paginated<CommentForView<T, CitizenRef>>
|
||||
|
||||
open fun findByParent(
|
||||
parent: CommentEntity<T>,
|
||||
parent: CommentForView<T, CitizenRef>,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<CommentEntity<T>> {
|
||||
): Paginated<CommentForView<T, CitizenRef>> {
|
||||
return findByParent(parent.id, page, limit)
|
||||
}
|
||||
|
||||
@@ -34,7 +30,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
|
||||
parentId: UUID,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<CommentEntity<T>> {
|
||||
): Paginated<CommentForView<T, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_parent")
|
||||
.select(
|
||||
@@ -49,7 +45,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
|
||||
): Paginated<CommentEntity<T>> {
|
||||
): Paginated<CommentForView<T, CitizenRef>> {
|
||||
return findByTarget(target.id, page, limit, sort)
|
||||
}
|
||||
|
||||
@@ -58,7 +54,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
|
||||
): Paginated<CommentEntity<T>> {
|
||||
): Paginated<CommentForView<T, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_target")
|
||||
.select(
|
||||
@@ -69,7 +65,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
|
||||
}
|
||||
}
|
||||
|
||||
fun <I : T> comment(comment: CommentEntity<I>) {
|
||||
fun <I : T, C: CitizenRef> comment(comment: CommentForUpdate<I, C>) {
|
||||
requester
|
||||
.getFunction("comment")
|
||||
.sendQuery(
|
||||
@@ -78,7 +74,7 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
|
||||
)
|
||||
}
|
||||
|
||||
fun <I : T> edit(comment: CommentEntity<I>) {
|
||||
fun <I : T> edit(comment: CommentForUpdate<I, CitizenRef>) {
|
||||
requester
|
||||
.getFunction("edit_comment")
|
||||
.sendQuery(
|
||||
@@ -89,17 +85,17 @@ abstract class Comment<T : TargetI>(override var requester: Requester) : Reposit
|
||||
}
|
||||
|
||||
class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
|
||||
override fun findById(id: UUID): CommentEntity<TargetRef>? {
|
||||
override fun findById(id: UUID): CommentForView<TargetRef, CitizenRef>? {
|
||||
return requester
|
||||
.getFunction("find_comment_by_id")
|
||||
.selectOne(mapOf("id" to id))
|
||||
}
|
||||
|
||||
override fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
citizen: CitizenI,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<TargetRef>> {
|
||||
): Paginated<CommentForView<TargetRef, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_citizen")
|
||||
.select(
|
||||
@@ -113,7 +109,7 @@ class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
|
||||
parentId: UUID,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<TargetRef>> {
|
||||
): Paginated<CommentForView<TargetRef, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_parent")
|
||||
.select(
|
||||
@@ -124,18 +120,18 @@ class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
|
||||
}
|
||||
}
|
||||
|
||||
class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
|
||||
override fun findById(id: UUID): CommentEntity<ArticleEntity>? {
|
||||
class CommentArticle(requester: Requester) : Comment<ArticleForView>(requester) {
|
||||
override fun findById(id: UUID): CommentForView<ArticleForView, CitizenRef>? {
|
||||
return requester
|
||||
.getFunction("find_comment_by_id")
|
||||
.selectOne(mapOf("id" to id))
|
||||
}
|
||||
|
||||
override fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
citizen: CitizenI,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<ArticleEntity>> {
|
||||
): Paginated<CommentForView<ArticleForView, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_citizen")
|
||||
.select(
|
||||
@@ -151,7 +147,7 @@ class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
|
||||
page: Int,
|
||||
limit: Int,
|
||||
sort: Sort
|
||||
): Paginated<CommentEntity<ArticleEntity>> = requester
|
||||
): Paginated<CommentForView<ArticleForView, CitizenRef>> = requester
|
||||
.getFunction("find_comments_by_target")
|
||||
.select(
|
||||
page, limit,
|
||||
@@ -171,17 +167,17 @@ class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
|
||||
}
|
||||
|
||||
class CommentConstitution(requester: Requester) : Comment<ConstitutionRef>(requester) {
|
||||
override fun findById(id: UUID): CommentEntity<ConstitutionRef>? {
|
||||
override fun findById(id: UUID): CommentForView<ConstitutionRef, CitizenRef>? {
|
||||
return requester
|
||||
.getFunction("find_comment_by_id")
|
||||
.selectOne(mapOf("id" to id))
|
||||
}
|
||||
|
||||
override fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
citizen: CitizenI,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<ConstitutionRef>> {
|
||||
): Paginated<CommentForView<ConstitutionRef, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_citizen")
|
||||
.select(
|
||||
@@ -197,7 +193,7 @@ class CommentConstitution(requester: Requester) : Comment<ConstitutionRef>(reque
|
||||
page: Int,
|
||||
limit: Int,
|
||||
sort: CommentArticle.Sort
|
||||
): Paginated<CommentEntity<ConstitutionRef>> {
|
||||
): Paginated<CommentForView<ConstitutionRef, CitizenRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_target")
|
||||
.select(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.CitizenSimple
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.CitizenWithUserI
|
||||
import fr.dcproject.entity.ConstitutionSimple
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
@@ -34,7 +34,7 @@ class Constitution(override var requester: Requester) : RepositoryI {
|
||||
)
|
||||
}
|
||||
|
||||
fun upsert(constitution: ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>>): ConstitutionEntity? {
|
||||
fun upsert(constitution: ConstitutionSimple<CitizenWithUserI, ConstitutionSimple.TitleSimple<ArticleRef>>): ConstitutionEntity? {
|
||||
return requester
|
||||
.getFunction("upsert_constitution")
|
||||
.selectOne("resource" to constitution)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.*
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.UuidEntity
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Constitution as ConstitutionEntity
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
|
||||
@@ -33,7 +34,7 @@ sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Req
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(follow: FollowEntity<IN>) {
|
||||
fun follow(follow: FollowForUpdate<IN, *>) {
|
||||
requester
|
||||
.getFunction("follow")
|
||||
.sendQuery(
|
||||
@@ -43,7 +44,7 @@ sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Req
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollow(follow: FollowEntity<IN>) {
|
||||
fun unfollow(follow: FollowForUpdate<IN, *>) {
|
||||
requester
|
||||
.getFunction("unfollow")
|
||||
.sendQuery(
|
||||
@@ -86,12 +87,12 @@ sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Req
|
||||
): Paginated<FollowSimple<IN, CitizenRef>>
|
||||
}
|
||||
|
||||
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(requester) {
|
||||
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleForView>(requester) {
|
||||
override fun findByCitizen(
|
||||
citizenId: UUID,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<FollowEntity<ArticleEntity>> {
|
||||
): Paginated<FollowEntity<ArticleForView>> {
|
||||
return requester.run {
|
||||
getFunction("find_follows_article_by_citizen")
|
||||
.select(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.OpinionChoiceRef
|
||||
import fr.dcproject.entity.OpinionForUpdate
|
||||
import fr.dcproject.entity.TargetRef
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
@@ -70,7 +71,7 @@ abstract class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requ
|
||||
fun updateOpinions(choice: OpinionChoiceRef, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>> =
|
||||
updateOpinions(listOf(choice), citizen, target)
|
||||
|
||||
abstract fun addOpinion(opinion: OpinionEntity<T>): OpinionEntity<T>
|
||||
abstract fun addOpinion(opinion: OpinionForUpdate<T>): OpinionEntity<T>
|
||||
|
||||
/**
|
||||
* Find opinions of one citizen filtered by target ids
|
||||
@@ -148,7 +149,7 @@ class OpinionArticle(requester: Requester) : Opinion<ArticleRef>(requester) {
|
||||
/**
|
||||
* Add Opinions on Article
|
||||
*/
|
||||
override fun addOpinion(opinion: OpinionEntity<ArticleRef>): OpinionArticleEntity {
|
||||
override fun addOpinion(opinion: OpinionForUpdate<ArticleRef>): OpinionArticleEntity {
|
||||
return requester
|
||||
.getFunction("upsert_opinion")
|
||||
.selectOne("resource" to opinion)!!
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.entity.Constitution
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
@@ -13,9 +12,8 @@ import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
|
||||
open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {
|
||||
fun vote(vote: VoteEntity<T>): VoteAggregation {
|
||||
fun vote(vote: VoteForUpdateI<T, *>, anonymous: Boolean? = null): VoteAggregation {
|
||||
val author = vote.createdBy
|
||||
val anonymous = author.voteAnonymous
|
||||
return requester
|
||||
.getFunction("vote")
|
||||
.selectOne(
|
||||
@@ -62,46 +60,46 @@ open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {
|
||||
}
|
||||
}
|
||||
|
||||
class VoteArticle(requester: Requester) : Vote<Article>(requester) {
|
||||
class VoteArticle(requester: Requester) : Vote<ArticleForView>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Article>> =
|
||||
): Paginated<VoteEntity<ArticleForView>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"article",
|
||||
object : TypeReference<List<VoteEntity<Article>>>() {},
|
||||
object : TypeReference<List<VoteEntity<ArticleForView>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
class VoteArticleComment(requester: Requester) : Vote<Comment<Article>>(requester) {
|
||||
class VoteArticleComment(requester: Requester) : Vote<CommentForView<ArticleForView, CitizenRef>>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Comment<Article>>> =
|
||||
): Paginated<VoteEntity<CommentForView<ArticleForView, CitizenRef>>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"article",
|
||||
object : TypeReference<List<VoteEntity<Comment<Article>>>>() {},
|
||||
object : TypeReference<List<VoteEntity<CommentForView<ArticleForView, CitizenRef>>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
class VoteComment(requester: Requester) : Vote<Comment<TargetRef>>(requester) {
|
||||
class VoteComment(requester: Requester) : Vote<CommentForView<TargetRef, CitizenRef>>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Comment<TargetRef>>> =
|
||||
): Paginated<VoteEntity<CommentForView<TargetRef, CitizenRef>>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"article",
|
||||
object : TypeReference<List<VoteEntity<Comment<TargetRef>>>>() {},
|
||||
object : TypeReference<List<VoteEntity<CommentForView<TargetRef, CitizenRef>>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.*
|
||||
import fr.dcproject.entity.Workgroup as WorkgroupEntity
|
||||
|
||||
class Workgroup(override var requester: Requester) : RepositoryI {
|
||||
fun findById(id: UUID): WorkgroupEntity? {
|
||||
fun findById(id: UUID): WorkgroupEntity<CitizenBasic>? {
|
||||
val function = requester.getFunction("find_workgroup_by_id")
|
||||
return function.selectOne("id" to id)
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class Workgroup(override var requester: Requester) : RepositoryI {
|
||||
direction: Direction? = null,
|
||||
search: String? = null,
|
||||
filter: Filter = Filter()
|
||||
): Paginated<WorkgroupEntity> {
|
||||
): Paginated<WorkgroupEntity<CitizenBasic>> {
|
||||
return requester
|
||||
.getFunction("find_workgroups")
|
||||
.select(
|
||||
@@ -37,11 +37,11 @@ class Workgroup(override var requester: Requester) : RepositoryI {
|
||||
)
|
||||
}
|
||||
|
||||
fun upsert(workgroup: WorkgroupSimple<CitizenRef>): WorkgroupEntity = requester
|
||||
fun <C: CitizenI, W: WorkgroupSimple<C>> upsert(workgroup: W): WorkgroupEntity<CitizenBasic> = requester
|
||||
.getFunction("upsert_workgroup")
|
||||
.selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null")
|
||||
|
||||
fun delete(workgroup: WorkgroupRef) = requester
|
||||
fun <W: WorkgroupRef> delete(workgroup: W) = requester
|
||||
.getFunction("delete_workgroup")
|
||||
.perform("id" to workgroup.id)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.entity.CommentForUpdate
|
||||
import fr.dcproject.entity.CommentRef
|
||||
import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.UPDATE
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.*
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.ktorVoter.assertCanAll
|
||||
import io.ktor.application.*
|
||||
@@ -67,7 +65,7 @@ fun Route.comment(repo: CommentRepository) {
|
||||
|
||||
post<CommentPaths.CreateCommentRequest> {
|
||||
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||
val newComment = Comment(
|
||||
val newComment = CommentForUpdate(
|
||||
content = call.receive<Content>().content,
|
||||
createdBy = citizen,
|
||||
parent = parent
|
||||
@@ -83,6 +81,7 @@ fun Route.comment(repo: CommentRepository) {
|
||||
val comment = repo.findById(it.comment.id)!!
|
||||
assertCan(UPDATE, comment)
|
||||
|
||||
|
||||
comment.content = call.receiveText()
|
||||
repo.edit(comment)
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.entity.CommentForUpdate
|
||||
import fr.dcproject.repository.CommentArticle.Sort
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
|
||||
@@ -14,14 +16,13 @@ import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CommentArticlePaths {
|
||||
@Location("/articles/{article}/comments")
|
||||
class ArticleCommentRequest(
|
||||
val article: Article,
|
||||
val article: ArticleRef,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
val search: String? = null,
|
||||
@@ -34,14 +35,14 @@ object CommentArticlePaths {
|
||||
|
||||
@Location("/articles/{article}/comments")
|
||||
class PostArticleCommentRequest(
|
||||
val article: Article
|
||||
val article: ArticleForView
|
||||
) {
|
||||
class Comment(
|
||||
val content: String
|
||||
)
|
||||
|
||||
suspend fun getComment(call: ApplicationCall) = call.receive<Comment>().run {
|
||||
CommentEntity(
|
||||
CommentForUpdate(
|
||||
target = article,
|
||||
createdBy = call.citizen,
|
||||
content = content
|
||||
|
||||
@@ -2,6 +2,7 @@ package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.entity.CommentForUpdate
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
|
||||
@@ -13,7 +14,6 @@ import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@@ -35,7 +35,7 @@ fun Route.commentConstitution(repo: CommentConstitutionRepository) {
|
||||
|
||||
post<CommentConstitutionPaths.ConstitutionCommentRequest> {
|
||||
val content = call.receiveText()
|
||||
val comment = CommentEntity(
|
||||
val comment = CommentForUpdate(
|
||||
target = it.constitution,
|
||||
createdBy = citizen,
|
||||
content = content
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.CitizenSimple
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.CitizenWithUserI
|
||||
import fr.dcproject.entity.ConstitutionSimple
|
||||
import fr.dcproject.entity.ConstitutionSimple.TitleSimple
|
||||
import fr.dcproject.security.voter.ConstitutionVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.ConstitutionVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.ktorVoter.assertCanAll
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.UuidEntity
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import io.ktor.application.*
|
||||
import io.ktor.locations.*
|
||||
@@ -58,18 +59,19 @@ object ConstitutionPaths {
|
||||
var rank: Int? = null,
|
||||
var articles: MutableList<ArticleRef> = mutableListOf()
|
||||
) : UuidEntity(id) {
|
||||
fun create(): ConstitutionSimple.TitleSimple<ArticleRef> =
|
||||
ConstitutionSimple.TitleSimple(
|
||||
fun create(): TitleSimple<ArticleRef> =
|
||||
TitleSimple(
|
||||
id, name, rank, articles
|
||||
)
|
||||
}
|
||||
|
||||
fun List<Title>.create(): MutableList<ConstitutionSimple.TitleSimple<ArticleRef>> =
|
||||
fun List<Title>.create(): MutableList<TitleSimple<ArticleRef>> =
|
||||
map { it.create() }.toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getNewConstitution(call: ApplicationCall): ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>> = call.receive<Constitution>().run {
|
||||
ConstitutionSimple(
|
||||
suspend fun getNewConstitution(call: ApplicationCall): ConstitutionSimple<CitizenWithUserI, TitleSimple<ArticleRef>> = call.receive<Constitution>().run {
|
||||
ConstitutionSimple<CitizenWithUserI, TitleSimple<ArticleRef>>(
|
||||
id = UUID.randomUUID(),
|
||||
title = title,
|
||||
titles = titles.create(),
|
||||
createdBy = call.citizen,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.DELETE
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.VIEW
|
||||
import fr.dcproject.entity.FollowForUpdate
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.*
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.ktorVoter.assertCanAll
|
||||
import io.ktor.application.*
|
||||
@@ -13,7 +12,6 @@ import io.ktor.http.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@@ -28,14 +26,14 @@ object FollowArticlePaths {
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.followArticle(repo: FollowArticleRepository) {
|
||||
post<FollowArticlePaths.ArticleFollowRequest> {
|
||||
val follow = FollowEntity(target = it.article, createdBy = this.citizen)
|
||||
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
||||
assertCan(CREATE, follow)
|
||||
repo.follow(follow)
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
|
||||
delete<FollowArticlePaths.ArticleFollowRequest> {
|
||||
val follow = FollowEntity(target = it.article, createdBy = this.citizen)
|
||||
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
|
||||
assertCan(DELETE, follow)
|
||||
repo.unfollow(follow)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
|
||||
@@ -3,9 +3,8 @@ package fr.dcproject.routes
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.DELETE
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.VIEW
|
||||
import fr.dcproject.entity.FollowForUpdate
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.*
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.ktorVoter.assertCanAll
|
||||
import io.ktor.application.*
|
||||
@@ -13,7 +12,6 @@ import io.ktor.http.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@@ -28,14 +26,14 @@ object FollowConstitutionPaths {
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.followConstitution(repo: FollowConstitutionRepository) {
|
||||
post<FollowConstitutionPaths.ConstitutionFollowRequest> {
|
||||
val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
|
||||
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
||||
assertCan(CREATE, follow)
|
||||
repo.follow(follow)
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
|
||||
delete<FollowConstitutionPaths.ConstitutionFollowRequest> {
|
||||
val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
|
||||
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
|
||||
assertCan(DELETE, follow)
|
||||
repo.unfollow(follow)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.OpinionChoiceRef
|
||||
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE
|
||||
@@ -18,7 +19,6 @@ import io.ktor.util.*
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
|
||||
|
||||
@@ -39,7 +39,7 @@ object OpinionArticlePaths {
|
||||
*/
|
||||
@Location("/articles/{article}/opinions")
|
||||
@KtorExperimentalAPI
|
||||
class ArticleOpinion(val article: ArticleEntity) {
|
||||
class ArticleOpinion(val article: ArticleForView) {
|
||||
class Body(ids: List<String>) {
|
||||
val ids = ids.map { OpinionChoiceRef(it.toUUID()) }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.entity.VoteForUpdate
|
||||
import fr.dcproject.repository.CommentGeneric
|
||||
import fr.dcproject.repository.VoteComment
|
||||
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
|
||||
@@ -18,14 +20,12 @@ import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
import fr.dcproject.repository.VoteArticle as VoteArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object VoteArticlePaths {
|
||||
@Location("/articles/{article}/vote")
|
||||
class ArticleVoteRequest(val article: ArticleEntity) {
|
||||
class ArticleVoteRequest(val article: ArticleForView) {
|
||||
data class Content(var note: Int)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ object VoteArticlePaths {
|
||||
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentGeneric) {
|
||||
put<ArticleVoteRequest> {
|
||||
val content = call.receive<ArticleVoteRequest.Content>()
|
||||
val vote = VoteEntity(
|
||||
val vote = VoteForUpdate(
|
||||
target = it.article,
|
||||
note = content.note,
|
||||
createdBy = this.citizen
|
||||
@@ -65,7 +65,7 @@ fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment,
|
||||
put<CommentVoteRequest> {
|
||||
val comment = commentRepo.findById(it.comment)!!
|
||||
val content = call.receive<CommentVoteRequest.Content>()
|
||||
val vote = VoteEntity(
|
||||
val vote = VoteForUpdate(
|
||||
target = comment,
|
||||
note = content.note,
|
||||
createdBy = this.citizen
|
||||
|
||||
@@ -2,19 +2,17 @@ package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.entity.VoteForUpdate
|
||||
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
|
||||
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
|
||||
import fr.ktorVoter.assertCan
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.put
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import fr.dcproject.entity.Constitution as ConstitutionEntity
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@@ -32,7 +30,7 @@ object VoteConstitutionPaths {
|
||||
fun Route.voteConstitution(repo: VoteConstitutionRepository) {
|
||||
put<VoteConstitutionPaths.ConstitutionVoteRequest> {
|
||||
val content = call.receive<Content>()
|
||||
val vote = VoteEntity(
|
||||
val vote = VoteForUpdate(
|
||||
target = it.constitution,
|
||||
note = content.note,
|
||||
createdBy = this.citizen
|
||||
|
||||
@@ -6,9 +6,9 @@ import fr.dcproject.entity.WorkgroupSimple
|
||||
import fr.dcproject.entity.WorkgroupWithMembersI.Member
|
||||
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
|
||||
import fr.dcproject.repository.Workgroup.Filter
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.Action.CREATE
|
||||
import fr.dcproject.routes.WorkgroupsPaths.PutWorkgroupRequest.Input
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.Action.*
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.Action.UPDATE
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.Action.VIEW
|
||||
import fr.dcproject.utils.toUUID
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.ktorVoter.assertCanAll
|
||||
@@ -19,9 +19,10 @@ import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Workgroup as WorkgroupEntity
|
||||
import fr.dcproject.repository.Workgroup as WorkgroupRepository
|
||||
import fr.dcproject.repository.Workgroup as WorkgroupRepo
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.ADD as ADD_MEMBERS
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.REMOVE as REMOVE_MEMBERS
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.UPDATE as UPDATE_MEMBERS
|
||||
@@ -43,8 +44,11 @@ object WorkgroupsPaths {
|
||||
val members: List<UUID>? = members?.toUUID()
|
||||
}
|
||||
|
||||
@Location("/workgroups/{workgroup}")
|
||||
class WorkgroupRequest(val workgroup: WorkgroupEntity)
|
||||
@Location("/workgroups/{workgroupId}")
|
||||
class WorkgroupRequest(private val workgroupId: UUID) : KoinComponent {
|
||||
val repo: WorkgroupRepo by inject()
|
||||
val workgroup = repo.findById(workgroupId) ?: TODO()
|
||||
}
|
||||
|
||||
@Location("/workgroups")
|
||||
open class PostWorkgroupRequest {
|
||||
@@ -68,31 +72,30 @@ object WorkgroupsPaths {
|
||||
}
|
||||
}
|
||||
|
||||
@Location("/workgroups/{workgroup}")
|
||||
class PutWorkgroupRequest(val workgroup: WorkgroupEntity) {
|
||||
class Body(
|
||||
@Location("/workgroups/{workgroupId}")
|
||||
class PutWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
|
||||
class Input(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val logo: String?,
|
||||
val anonymous: Boolean?
|
||||
)
|
||||
|
||||
suspend fun updateWorkgroup(call: ApplicationCall): Unit = call.receive<Body>().run {
|
||||
name?.let { workgroup.name = it }
|
||||
description?.let { workgroup.description = it }
|
||||
logo?.let { workgroup.logo = it }
|
||||
anonymous?.let { workgroup.anonymous = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Location("/workgroups/{workgroup}")
|
||||
class DeleteWorkgroupRequest(val workgroup: WorkgroupEntity)
|
||||
@Location("/workgroups/{workgroupId}")
|
||||
class DeleteWorkgroupRequest(val workgroupId: UUID) : KoinComponent {
|
||||
val repo: WorkgroupRepo by inject()
|
||||
val workgroup = repo.findById(workgroupId)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object WorkgroupsMembersPaths {
|
||||
@Location("/workgroups/{workgroup}/members")
|
||||
class WorkgroupsMembersRequest(val workgroup: WorkgroupEntity) {
|
||||
@Location("/workgroups/{workgroupId}/members")
|
||||
class WorkgroupsMembersRequest(val workgroupId: UUID) : KoinComponent {
|
||||
val repo: WorkgroupRepo by inject()
|
||||
val workgroup = repo.findById(workgroupId)
|
||||
|
||||
class Body : MutableList<Body.Item> by mutableListOf() {
|
||||
class Item(val citizen: CitizenRef, roles: List<String> = emptyList()) {
|
||||
val roles: List<Role> = roles.map {
|
||||
@@ -111,7 +114,7 @@ object WorkgroupsMembersPaths {
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.workgroup(repo: WorkgroupRepository) {
|
||||
fun Route.workgroup(repo: WorkgroupRepo) {
|
||||
get<WorkgroupsPaths.WorkgroupsRequest> {
|
||||
val workgroups =
|
||||
repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy, members = it.members))
|
||||
@@ -136,50 +139,74 @@ fun Route.workgroup(repo: WorkgroupRepository) {
|
||||
}
|
||||
|
||||
put<WorkgroupsPaths.PutWorkgroupRequest> {
|
||||
it.updateWorkgroup(call).let { workgroup ->
|
||||
assertCan(UPDATE, workgroup)
|
||||
repo.upsert(workgroup as WorkgroupSimple<CitizenRef>)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.OK, it)
|
||||
}
|
||||
repo.findById(it.workgroupId)?.let { old ->
|
||||
call.receive<Input>().run {
|
||||
old.copy(
|
||||
name = name ?: old.name,
|
||||
description = description ?: old.description,
|
||||
logo = logo ?: old.logo,
|
||||
anonymous = anonymous ?: old.anonymous
|
||||
).let { workgroup ->
|
||||
assertCan(UPDATE, workgroup)
|
||||
repo.upsert(workgroup)
|
||||
call.respond(HttpStatusCode.OK, it)
|
||||
}
|
||||
}
|
||||
} ?: call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
delete<WorkgroupsPaths.DeleteWorkgroupRequest> {
|
||||
assertCan(UPDATE, it.workgroup)
|
||||
repo.delete(it.workgroup)
|
||||
call.respond(HttpStatusCode.NoContent, it)
|
||||
if (it.workgroup != null) {
|
||||
assertCan(DELETE, it.workgroup)
|
||||
repo.delete(it.workgroup)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
/* Add members to workgroup */
|
||||
post<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(ADD_MEMBERS, it.workgroup)
|
||||
repo.addMembers(it.workgroup, members)
|
||||
}.let { members ->
|
||||
call.respond(HttpStatusCode.Created, members)
|
||||
}
|
||||
if (it.workgroup != null) {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(ADD_MEMBERS, it.workgroup)
|
||||
repo.addMembers(it.workgroup, members)
|
||||
}.let { members ->
|
||||
call.respond(HttpStatusCode.Created, members)
|
||||
}
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete members of workgroup */
|
||||
delete<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(REMOVE_MEMBERS, it.workgroup)
|
||||
repo.removeMembers(it.workgroup, members)
|
||||
}.let { members ->
|
||||
call.respond(HttpStatusCode.OK, members)
|
||||
}
|
||||
if (it.workgroup != null) {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(REMOVE_MEMBERS, it.workgroup)
|
||||
repo.removeMembers(it.workgroup, members)
|
||||
}.let { members ->
|
||||
call.respond(HttpStatusCode.OK, members)
|
||||
}
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
/* Update members of workgroup */
|
||||
put<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(UPDATE_MEMBERS, it.workgroup)
|
||||
repo.updateMembers(it.workgroup, members)
|
||||
}.let { members ->
|
||||
call.respond(HttpStatusCode.OK, members)
|
||||
}
|
||||
if (it.workgroup != null) {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(UPDATE_MEMBERS, it.workgroup)
|
||||
repo.updateMembers(it.workgroup, members)
|
||||
}.let { members ->
|
||||
call.respond(HttpStatusCode.OK, members)
|
||||
}
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ package fr.dcproject.utils
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.format.ISODateTimeFormat
|
||||
|
||||
fun DateTime.toIso() = ISODateTimeFormat.dateTime().print(this)
|
||||
fun DateTime.toIso(): String = ISODateTimeFormat.dateTime().print(this)
|
||||
@@ -6,5 +6,5 @@ import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
|
||||
override fun getValue(thisRef: R, property: KProperty<*>) = LoggerFactory.getLogger(thisRef.javaClass.packageName)
|
||||
override fun getValue(thisRef: R, property: KProperty<*>): Logger = LoggerFactory.getLogger(thisRef.javaClass.packageName)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package fr.dcproject.utils
|
||||
|
||||
fun String.readResource(callbak: (String) -> Unit = {}): String {
|
||||
val content = callbak::class.java.getResource(this).readText()
|
||||
callbak(content)
|
||||
fun String.readResource(callback: (String) -> Unit = {}): String {
|
||||
val content = callback::class.java.getResource(this).readText()
|
||||
callback(content)
|
||||
return content
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
package fr.dcproject.views
|
||||
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.CitizenI
|
||||
import fr.dcproject.entity.ViewAggregation
|
||||
import org.elasticsearch.client.Response
|
||||
import org.joda.time.DateTime
|
||||
|
||||
interface ViewManager <T> {
|
||||
fun addView(ip: String, entity: T, citizen: CitizenRef? = null, dateTime: DateTime = DateTime.now()): Response?
|
||||
/**
|
||||
* Add view to one entity
|
||||
*/
|
||||
fun addView(ip: String, entity: T, citizen: CitizenI? = null, dateTime: DateTime = DateTime.now()): Response?
|
||||
|
||||
/**
|
||||
* Get Views aggregations
|
||||
*/
|
||||
fun getViewsCount(entity: T): ViewAggregation
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import fr.dcproject.entity.CitizenBasicI
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.entity.CitizenWithUserI
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import fr.dcproject.voter.NoRuleDefinedException
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import io.ktor.application.*
|
||||
import io.ktor.locations.*
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
class CitizenVoter : Voter<ApplicationCall> {
|
||||
@@ -19,45 +19,43 @@ class CitizenVoter : Voter<ApplicationCall> {
|
||||
CHANGE_PASSWORD
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if (!((action is Action)
|
||||
&& (subject is CitizenBasicI?))) return Vote.ABSTAIN
|
||||
&& (subject is CitizenBasicI?))) return abstain()
|
||||
|
||||
val user = context.user
|
||||
if (action == Action.CREATE && user != null) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (user == null) return Vote.DENIED
|
||||
if (user == null) return denied("You must be connected to view citizen", "citizen.view.connected")
|
||||
if (subject is CitizenBasicI) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted")
|
||||
else granted()
|
||||
}
|
||||
return Vote.DENIED
|
||||
throw NoRuleDefinedException(action)
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return Vote.DENIED
|
||||
return denied("You can never deleted a citizen", "citizen.delete.never")
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE &&
|
||||
user is UserI &&
|
||||
subject is CitizenBasicI &&
|
||||
subject.user.id == user.id
|
||||
) {
|
||||
return Vote.GRANTED
|
||||
if (action == Action.UPDATE) {
|
||||
if (user == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected")
|
||||
if (subject !is CitizenWithUserI) throw NoSubjectDefinedException(action)
|
||||
return if (subject.user.id == user.id) granted() else denied("You can only update your citizen", "citizen.update.notYours")
|
||||
}
|
||||
|
||||
if (action == Action.CHANGE_PASSWORD && user != null && subject is CitizenBasicI) {
|
||||
val userToChange = subject.user
|
||||
return if (user.id == userToChange.id) {
|
||||
Vote.GRANTED
|
||||
granted()
|
||||
} else {
|
||||
Vote.DENIED
|
||||
denied("You can only change your password", "citizen.password.notYours")
|
||||
}
|
||||
}
|
||||
|
||||
return Vote.DENIED
|
||||
throw NoRuleDefinedException(action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.citizenOrNull
|
||||
import fr.dcproject.entity.CommentForUpdate
|
||||
import fr.dcproject.entity.CommentForView
|
||||
import fr.dcproject.entity.CommentI
|
||||
import fr.dcproject.voter.NoRuleDefinedException
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import fr.postgresjson.entity.EntityDeletedAt
|
||||
import io.ktor.application.*
|
||||
|
||||
class CommentVoter : Voter<ApplicationCall> {
|
||||
enum class Action : ActionI {
|
||||
@@ -15,38 +18,47 @@ class CommentVoter : Voter<ApplicationCall> {
|
||||
DELETE
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
if (!(action is Action && subject is Comment<*>?)) return Vote.ABSTAIN
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if (!(action is Action && subject is CommentI?)) return abstain()
|
||||
|
||||
val user = context.user
|
||||
val citizen = context.citizenOrNull
|
||||
|
||||
if (subject == null) {
|
||||
return Vote.DENIED
|
||||
throw NoSubjectDefinedException(action)
|
||||
}
|
||||
|
||||
if (action == Action.CREATE) {
|
||||
if (user == null) {
|
||||
return Vote.DENIED
|
||||
return when {
|
||||
citizen == null -> denied("You must be connected to create user", "comment.create.notConnected")
|
||||
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
|
||||
subject.createdBy.id != citizen.id -> denied("You cannot create a comment with other user than yours", "comment.create.wrongUser")
|
||||
subject.parent?.isDeleted() ?: false -> denied("You cannot create a comment on deleted parent", "comment.create.deletedParent")
|
||||
subject.target.let { it is EntityDeletedAt && it.isDeleted() } -> denied("You cannot create a comment on deleted target", "comment.create.deletedTarget")
|
||||
else -> granted()
|
||||
}
|
||||
if (subject.createdBy.user.id != user.id) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
return when {
|
||||
subject !is CommentForView<*, *> -> throw NoSubjectDefinedException(action)
|
||||
subject.isDeleted() -> denied("Your cannot view a deleted comment", "comment.view.deleted")
|
||||
else -> granted()
|
||||
}
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE && user != null && user.id == subject.createdBy.user.id) {
|
||||
return Vote.GRANTED
|
||||
if (action == Action.UPDATE) {
|
||||
if (citizen == null) return denied("You must be connected to update comment", "comment.update.notConnected")
|
||||
return when {
|
||||
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
|
||||
citizen.id == subject.createdBy.id -> granted()
|
||||
else -> denied("You cannot update another user of yours", "comment.update.notYours")
|
||||
}
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return Vote.DENIED
|
||||
return denied("A comment can never be deleted", "comment.deleted.never")
|
||||
}
|
||||
|
||||
return Vote.DENIED
|
||||
throw NoRuleDefinedException(action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.entity.CommentForView
|
||||
import fr.dcproject.entity.ConstitutionSimple
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.voter.NoRuleDefinedException
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import io.ktor.application.*
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
|
||||
class ConstitutionVoter : Voter<ApplicationCall> {
|
||||
@@ -18,63 +18,63 @@ class ConstitutionVoter : Voter<ApplicationCall> {
|
||||
DELETE
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if(!((action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
|
||||
&& (subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is Comment<*>))) return Vote.ABSTAIN
|
||||
&& (subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is CommentForView<*, *>))) return abstain()
|
||||
|
||||
val user = context.user
|
||||
if (action == Action.CREATE && user != null) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is ConstitutionSimple<*, *>) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
return if (subject.isDeleted()) denied("You cannot view a deleted constitution", "constitution.view.deleted")
|
||||
else granted()
|
||||
}
|
||||
return Vote.DENIED
|
||||
throw NoSubjectDefinedException(action as ActionI)
|
||||
}
|
||||
|
||||
if (action == Action.DELETE && user is UserI && subject is ConstitutionSimple<*, *> && subject.createdBy.user.id == user.id) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE && user is UserI && subject is ConstitutionSimple<*, *> && subject.createdBy.user.id == user.id) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
|
||||
if (action is CommentVoter.Action) return voteForComment(action)
|
||||
if (action is VoteVoter.Action) return voteForVote(action, subject)
|
||||
|
||||
if (action is Action) {
|
||||
return Vote.DENIED
|
||||
throw NoRuleDefinedException(action)
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
|
||||
private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote {
|
||||
private fun voteForVote(action: VoteVoter.Action, subject: Any?): VoterResponseI {
|
||||
if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
|
||||
val target = subject.target
|
||||
if (target !is ConstitutionSimple<*, *>) {
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
if (target.isDeleted()) {
|
||||
return Vote.DENIED
|
||||
return denied("You cannot vote a deleted constitution", "constitution.vote.deleted")
|
||||
}
|
||||
}
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
|
||||
private fun voteForComment(action: CommentVoter.Action): Vote {
|
||||
private fun voteForComment(action: CommentVoter.Action): VoterResponseI {
|
||||
if (action == CommentVoter.Action.CREATE) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
|
||||
if (action == CommentVoter.Action.VIEW) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.citizenOrNull
|
||||
import fr.dcproject.entity.CitizenI
|
||||
import fr.dcproject.entity.FollowI
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import io.ktor.application.*
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
import fr.dcproject.entity.User as UserEntity
|
||||
|
||||
class FollowVoter : Voter<ApplicationCall> {
|
||||
enum class Action : ActionI {
|
||||
@@ -15,33 +15,33 @@ class FollowVoter : Voter<ApplicationCall> {
|
||||
VIEW
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
if (!((action is Action)
|
||||
&& (subject is FollowEntity<*>?))) return Vote.ABSTAIN
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if (action !is Action) return abstain()
|
||||
if (subject !is FollowI) throw NoSubjectDefinedException(action)
|
||||
|
||||
val user = context.user
|
||||
val citizen = context.citizenOrNull
|
||||
if (action == Action.CREATE) {
|
||||
return if (user != null) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
return if (citizen == null) denied("You must be connected to follow", "follow.create.notConnected")
|
||||
else granted()
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return if (user != null) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
return if (citizen == null) denied("You must be connected to unfollow", "follow.delete.notConnected")
|
||||
else granted()
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is FollowEntity<*>) {
|
||||
return voteView(user, subject)
|
||||
return voteView(citizen, subject)
|
||||
}
|
||||
return Vote.DENIED
|
||||
throw NoSubjectDefinedException(action)
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
|
||||
private fun voteView(user: UserEntity?, subject: FollowEntity<*>): Vote {
|
||||
return if ((user != null && subject.createdBy.user.id == user.id) || !subject.createdBy.followAnonymous) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
private fun voteView(citizen: CitizenI?, subject: FollowEntity<*>): VoterResponseI {
|
||||
return if ((citizen != null && subject.createdBy.id == citizen.id) || !subject.createdBy.followAnonymous) granted()
|
||||
else denied("You cannot view an anonymous follow", "follow.view.anonymous")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import fr.dcproject.entity.OpinionChoice
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import io.ktor.application.*
|
||||
|
||||
class OpinionChoiceVoter : Voter<ApplicationCall> {
|
||||
enum class Action : ActionI {
|
||||
VIEW
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if (!((action is Action)
|
||||
&& (subject is OpinionChoice?))) return Vote.ABSTAIN
|
||||
&& (subject is OpinionChoice?))) return abstain()
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is OpinionChoice) {
|
||||
return Vote.GRANTED
|
||||
return granted()
|
||||
}
|
||||
return Vote.DENIED
|
||||
throw NoSubjectDefinedException(action)
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.entity.ArticleAuthI
|
||||
import fr.dcproject.component.article.ArticleAuthI
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.entity.Opinion
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Vote.Companion.toVote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.voter.NoRuleDefinedException
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import io.ktor.application.*
|
||||
|
||||
class OpinionVoter : Voter<ApplicationCall> {
|
||||
enum class Action : ActionI {
|
||||
@@ -17,32 +16,33 @@ class OpinionVoter : Voter<ApplicationCall> {
|
||||
DELETE
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if (!((action is Action)
|
||||
&& (subject is Opinion<*>? || subject is ArticleAuthI<*>))) return Vote.ABSTAIN
|
||||
&& (subject is Opinion<*>? || subject is ArticleAuthI<*>))) return abstain()
|
||||
|
||||
val user = context.user
|
||||
if (action == Action.CREATE) {
|
||||
return toVote {
|
||||
user != null && (
|
||||
(subject is ArticleAuthI<*> && !subject.isDeleted()) ||
|
||||
(subject is Opinion<*> && subject.createdBy.user.id == user.id)
|
||||
)
|
||||
}
|
||||
if (user == null) return denied("You must be connected to make an opinion", "opinion.create.notConnected")
|
||||
if (subject is ArticleAuthI<*> && !subject.isDeleted()) return granted()
|
||||
if (subject is Opinion<*> && subject.createdBy.user.id == user.id) return granted()
|
||||
|
||||
throw NoSubjectDefinedException(action)
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
return toVote { subject is Opinion<*> || subject is Article }
|
||||
return if (subject is Opinion<*> || subject is ArticleForView) granted() else throw NoSubjectDefinedException(action)
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return toVote {
|
||||
subject is Opinion<*> &&
|
||||
user != null &&
|
||||
subject.createdBy.user.id == user.id
|
||||
}
|
||||
if (user == null) return denied("You must be connected to delete opinion", "opinion.delete.notConnected")
|
||||
if (subject !is Opinion<*>) throw NoSubjectDefinedException(action)
|
||||
return if (subject.createdBy.user.id == user.id) granted() else denied("You can only delete your opinions", "opinion.delete.notYours")
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
if (action is Action) {
|
||||
throw NoRuleDefinedException(action)
|
||||
}
|
||||
|
||||
return abstain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.citizenOrNull
|
||||
import fr.dcproject.entity.VoteForUpdateI
|
||||
import fr.dcproject.entity.VoteI
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import fr.postgresjson.entity.EntityDeletedAt
|
||||
import io.ktor.application.*
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
|
||||
class VoteVoter : Voter<ApplicationCall> {
|
||||
@@ -13,26 +15,36 @@ class VoteVoter : Voter<ApplicationCall> {
|
||||
VIEW
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
if (!(action is Action && subject is VoteEntity<*>?)) return Vote.ABSTAIN
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
|
||||
if (!(action is Action && subject is VoteI)) return abstain()
|
||||
|
||||
val user = context.user ?: return Vote.DENIED
|
||||
val citizen = context.citizenOrNull ?: return denied("You must be connected for vote", "vote.connected")
|
||||
|
||||
if (action == Action.CREATE) {
|
||||
return Vote.GRANTED
|
||||
if (subject !is VoteForUpdateI<*, *>) throw NoSubjectDefinedException(action)
|
||||
subject.target.let {
|
||||
if (it is EntityDeletedAt) {
|
||||
if (it.isDeleted()) return denied("You cannot vote on deleted target", "vote.create.isDeleted")
|
||||
} else {
|
||||
throw NoSubjectDefinedException(action)
|
||||
}
|
||||
}
|
||||
return granted()
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is VoteEntity<*>) {
|
||||
return if (subject.createdBy.user.id != user.id) {
|
||||
Vote.DENIED
|
||||
return if (subject.createdBy.id != citizen.id) {
|
||||
denied("You can view only your votes", "vote.view")
|
||||
} else {
|
||||
Vote.GRANTED
|
||||
granted()
|
||||
}
|
||||
} else {
|
||||
throw NoSubjectDefinedException(action)
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
return abstain()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.entity.WorkgroupI
|
||||
import fr.dcproject.entity.WorkgroupWithAuthI
|
||||
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import fr.ktorVoter.VoterException
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.voter.NoRuleDefinedException
|
||||
import fr.dcproject.voter.NoSubjectDefinedException
|
||||
import fr.ktorVoter.*
|
||||
import io.ktor.application.*
|
||||
|
||||
class WorkgroupVoter : Voter<ApplicationCall> {
|
||||
enum class Action : ActionI {
|
||||
@@ -24,67 +25,72 @@ class WorkgroupVoter : Voter<ApplicationCall> {
|
||||
REMOVE,
|
||||
}
|
||||
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): Vote {
|
||||
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
|
||||
if ((action is Action && subject == null)) throw NoSubjectDefinedException(action)
|
||||
if (!((action is Action || action is ActionMembers)
|
||||
&& (subject is WorkgroupI? || (subject is List<*> && subject.first() is WorkgroupI)))) return Vote.ABSTAIN
|
||||
&& (subject is WorkgroupI? || (subject is List<*> && subject.first() is WorkgroupI)))) return abstain()
|
||||
|
||||
val user = context.user
|
||||
if (subject is WorkgroupI && action == Action.CREATE && user is UserI) {
|
||||
return Vote.GRANTED
|
||||
if (action == Action.CREATE) {
|
||||
if (user == null) return denied("You must be connected to delete workgroup", "workgroup.delete.notConnected")
|
||||
if (subject is WorkgroupI) {
|
||||
return granted()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is WorkgroupWithAuthI<*>) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else if (!subject.anonymous) Vote.GRANTED
|
||||
else if (subject.anonymous && user != null && subject.isMember(user)) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
return if (subject.isDeleted()) denied("You cannot view a deleted workgroup", "workgroup.view.deleted")
|
||||
else if (!subject.anonymous) granted()
|
||||
else if (subject.anonymous && user != null && subject.isMember(user)) granted()
|
||||
else denied("You cannot view anonymous workgroup", "workgroup.view.anonymous")
|
||||
}
|
||||
return Vote.DENIED
|
||||
throw NoSubjectDefinedException(action as ActionI)
|
||||
}
|
||||
|
||||
if (subject is WorkgroupWithAuthI<*>) {
|
||||
if (action == Action.DELETE && user is UserI && subject.hasRole(Role.MASTER, user)) {
|
||||
return Vote.GRANTED
|
||||
if (subject is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
|
||||
if (action == Action.DELETE) {
|
||||
if (user == null) return denied("You must be connected to delete workgroup", "workgroup.delete.notConnected")
|
||||
return if (subject.hasRole(Role.MASTER, user)) granted()
|
||||
else denied("You must hase role MASTER to delete workgroup", "workgroup.delete.role")
|
||||
}
|
||||
if (action == Action.UPDATE) {
|
||||
if (user == null) return denied("You must be connected to delete workgroup", "workgroup.delete.notConnected")
|
||||
return if (subject.hasRole(Role.MASTER, user)) granted()
|
||||
else denied("You must hase role MASTER to delete workgroup", "workgroup.delete.role")
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE && user is UserI && subject.hasRole(Role.MASTER, user)) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
return Vote.DENIED
|
||||
throw NoRuleDefinedException(action as ActionI)
|
||||
} else if (subject !is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
|
||||
throw object :
|
||||
VoterException("Unable to define if your are granted, the subject must implement 'WorkgroupWithAuthI'") {}
|
||||
throw NoSubjectDefinedException(action as ActionI)
|
||||
}
|
||||
|
||||
if (action == ActionMembers.ADD) {
|
||||
// TODO create ROLES
|
||||
return Vote.toVote {
|
||||
user is UserI &&
|
||||
subject is WorkgroupWithAuthI<*> &&
|
||||
subject.hasRole(Role.MASTER, user)
|
||||
}
|
||||
if (user !is UserI) return denied("You must be connected to add member to the workgroup", "workgroup.addMember.notConnected")
|
||||
if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
|
||||
return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for add member to workgroup", "workgroup.addMember.role")
|
||||
}
|
||||
|
||||
if (action == ActionMembers.UPDATE) {
|
||||
// TODO create ROLES
|
||||
return Vote.toVote {
|
||||
user is UserI &&
|
||||
subject is WorkgroupWithAuthI<*> &&
|
||||
subject.hasRole(Role.MASTER, user)
|
||||
}
|
||||
if (user !is UserI) return denied("You must be connected to update member of the workgroup", "workgroup.updateMember.notConnected")
|
||||
if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
|
||||
return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for update members of workgroup", "workgroup.updateMember.role")
|
||||
}
|
||||
|
||||
if (action == ActionMembers.REMOVE) {
|
||||
// TODO create ROLES
|
||||
return Vote.toVote {
|
||||
user is UserI &&
|
||||
subject is WorkgroupWithAuthI<*> &&
|
||||
subject.hasRole(Role.MASTER, user)
|
||||
}
|
||||
if (user !is UserI) return denied("You must be connected to remove member of the workgroup", "workgroup.removeMember.notConnected")
|
||||
if (subject !is WorkgroupWithAuthI<*>) throw NoSubjectDefinedException(action as ActionI)
|
||||
return if (subject.hasRole(Role.MASTER, user)) granted() else denied("You must have MASTER Role for remove members of workgroup", "workgroup.removeMember.role")
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
if (action is Action) {
|
||||
throw NoRuleDefinedException(action)
|
||||
}
|
||||
|
||||
return abstain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,8 +1342,6 @@ components:
|
||||
properties:
|
||||
user:
|
||||
$ref: '#/components/schemas/UserResponse'
|
||||
workgroups:
|
||||
$ref: '#/components/schemas/WorkgroupSimple'
|
||||
CitizenBase:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1375,8 +1373,6 @@ components:
|
||||
properties:
|
||||
user:
|
||||
$ref: '#/components/schemas/UserRequest'
|
||||
workgroups:
|
||||
$ref: '#/components/schemas/UuidEntity'
|
||||
|
||||
RegisterRequest:
|
||||
$ref: '#/components/schemas/CitizenRequest'
|
||||
|
||||
@@ -6,7 +6,16 @@ begin
|
||||
select to_json(t)
|
||||
from (
|
||||
select
|
||||
a.*,
|
||||
a.id,
|
||||
a.version_number,
|
||||
a.version_id,
|
||||
a.title,
|
||||
a.anonymous,
|
||||
a.content,
|
||||
a.description,
|
||||
a.tags,
|
||||
a.draft,
|
||||
a.last_version,
|
||||
find_citizen_by_id_with_user(a.created_by_id) as created_by,
|
||||
find_workgroup_by_id(a.workgroup_id) as workgroup,
|
||||
count_vote(a.id) as votes,
|
||||
|
||||
@@ -21,11 +21,13 @@ begin
|
||||
into resource, total
|
||||
from (
|
||||
select
|
||||
a.*,
|
||||
a.id,
|
||||
a.title,
|
||||
a.deleted_at,
|
||||
a.draft,
|
||||
find_citizen_by_id_with_user(a.created_by_id) as created_by,
|
||||
find_workgroup_by_id(a.workgroup_id) as workgroup,
|
||||
count_vote(a.id) as votes,
|
||||
count_opinion(a.id) as opinions,
|
||||
zdb.score(a.ctid) _score
|
||||
from article as a
|
||||
left join vote_cache ca using (id)
|
||||
|
||||
@@ -54,10 +54,10 @@ begin
|
||||
|
||||
insert into article_relations (source_id, target_id, created_by_id)
|
||||
select
|
||||
(resource->>'id')::uuid,
|
||||
(rel->>'id')::uuid,
|
||||
id,
|
||||
(resource#>>'{created_by, id}')::uuid
|
||||
from json_populate_recordset(null::article, resource->>'relations');
|
||||
(rel#>>'{created_by, id}')::uuid
|
||||
from json_populate_recordset(null::article, resource->>'relations') rel;
|
||||
end if;
|
||||
|
||||
select find_article_by_id(new_id) into resource;
|
||||
|
||||
@@ -8,7 +8,6 @@ begin
|
||||
select
|
||||
z.*
|
||||
from citizen as z
|
||||
left join citizen_in_workgroup ciw on z.id = ciw.citizen_id
|
||||
where z.id = _id
|
||||
group by z.id
|
||||
) as t;
|
||||
|
||||
@@ -9,7 +9,6 @@ begin
|
||||
z.*,
|
||||
find_user_by_id(z.user_id) as "user"
|
||||
from citizen as z
|
||||
left join citizen_in_workgroup ciw on z.id = ciw.citizen_id
|
||||
where z.id = _id
|
||||
group by z.id
|
||||
) as t;
|
||||
|
||||
@@ -9,6 +9,8 @@ begin
|
||||
from (
|
||||
select
|
||||
com.*,
|
||||
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count,
|
||||
find_comment_parent_by_id(com.parent_id) as parent,
|
||||
find_reference_by_id(com.target_id, com.target_reference) as target,
|
||||
find_citizen_by_id_with_user(com.created_by_id) as created_by,
|
||||
count_vote(com.id) as votes
|
||||
|
||||
@@ -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 (
|
||||
select
|
||||
com.*,
|
||||
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count,
|
||||
find_comment_parent_by_id(com.parent_id) as parent,
|
||||
find_reference_by_id(com.target_id, _reference) as target,
|
||||
find_citizen_by_id_with_user(com.created_by_id) as created_by,
|
||||
count_vote(com.id) as votes
|
||||
|
||||
@@ -13,6 +13,7 @@ begin
|
||||
select
|
||||
com.*,
|
||||
(select count(*) from "comment" c2 where c2.parents_ids @> array[com.id]) as children_count,
|
||||
find_comment_parent_by_id(com.parent_id) as parent,
|
||||
find_reference_by_id(com.target_id, com.target_reference) as target,
|
||||
find_citizen_by_id_with_user(com.created_by_id) as created_by,
|
||||
count_vote(com.id) as votes
|
||||
|
||||
@@ -14,6 +14,7 @@ begin
|
||||
select
|
||||
com.*,
|
||||
(select count(c2) from "comment" c2 where c2.parent_comment_id = com.id) as children_count,
|
||||
find_comment_parent_by_id(com.parent_id) as parent,
|
||||
find_reference_by_id(com.target_id, com.target_reference) as target,
|
||||
find_citizen_by_id_with_user(com.created_by_id) as created_by,
|
||||
count_vote(com.id) as votes
|
||||
|
||||
@@ -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
|
||||
$$
|
||||
declare _anonymous_conf bool = coalesce(_anonymous, (select vote_anonymous from citizen where id = _created_by_id));
|
||||
begin
|
||||
if reference = 'article'::regclass then
|
||||
insert into vote_for_article (created_by_id, target_id, note, anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous_conf)
|
||||
on conflict (created_by_id, target_id) do update set
|
||||
note = excluded.note,
|
||||
anonymous = excluded.anonymous,
|
||||
updated_at = now();
|
||||
elseif reference = 'constitution'::regclass then
|
||||
insert into vote_for_constitution (created_by_id, target_id, note, anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous_conf)
|
||||
on conflict (created_by_id, target_id) do update set
|
||||
note = excluded.note,
|
||||
anonymous = excluded.anonymous,
|
||||
updated_at = now();
|
||||
elseif reference = 'comment_on_article'::regclass then
|
||||
insert into vote_for_comment_on_article (created_by_id, target_id, note, anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous_conf)
|
||||
on conflict (created_by_id, target_id) do update set
|
||||
note = excluded.note,
|
||||
anonymous = excluded.anonymous,
|
||||
updated_at = now();
|
||||
elseif reference = 'comment_on_constitution'::regclass then
|
||||
insert into vote_for_comment_on_constitution (created_by_id, target_id, note, anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous)
|
||||
values (_created_by_id, _target_id, _note, _anonymous_conf)
|
||||
on conflict (created_by_id, target_id) do update set
|
||||
note = excluded.note,
|
||||
anonymous = excluded.anonymous,
|
||||
|
||||
@@ -23,9 +23,9 @@ begin
|
||||
insert into citizen_in_workgroup (workgroup_id, citizen_id, roles)
|
||||
select
|
||||
new_id::uuid,
|
||||
citizen_id,
|
||||
roles
|
||||
from json_populate_recordset(null::citizen_in_workgroup, resource->'members') m;
|
||||
(m#>>'{citizen,id}')::uuid,
|
||||
json_to_array(m#>'{roles}')
|
||||
from json_array_elements(resource->'members') m;
|
||||
|
||||
-- insert master if no members
|
||||
if (exists) then
|
||||
|
||||
@@ -11,6 +11,7 @@ drop table if exists follow_constitution;
|
||||
drop table if exists follow_citizen;
|
||||
drop table if exists follow;
|
||||
|
||||
drop table if exists vote_cache;
|
||||
drop table if exists vote_for_article;
|
||||
drop table if exists vote_for_constitution;
|
||||
drop table if exists vote_for_comment_on_article;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import fr.dcproject.entity.CitizenBasic
|
||||
import fr.dcproject.entity.CitizenI
|
||||
import fr.dcproject.entity.Constitution
|
||||
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.`should equal`
|
||||
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 ConstitutionTest {
|
||||
@Language("JSON")
|
||||
private val constitutionJson: String = """{
|
||||
"id":"15814bb6-8d90-4c6a-a456-c3939a8ec75e",
|
||||
"title":"Hello world!",
|
||||
"anonymous":true,
|
||||
"titles":[
|
||||
{
|
||||
"id":"8156b66f-a9c8-4fd9-8375-a8a1f42ccfd2",
|
||||
"name":"plop",
|
||||
"rank":0,
|
||||
"created_by":{
|
||||
"id":"18902d22-245d-4d44-b23d-9f0e82688612",
|
||||
"name":{
|
||||
"first_name":"Jaque",
|
||||
"last_name":"Bono",
|
||||
"civility":null
|
||||
},
|
||||
"email": "jaque.bono@gmail.com",
|
||||
"birthday":"2019-08-07T20:34:08.013Z",
|
||||
"user_id":null,
|
||||
"vote_anonymous":null,
|
||||
"follow_anonymous":null,
|
||||
"user":{
|
||||
"id":"257abe9f-be17-4ad3-ae6a-b1dc9706d5d7",
|
||||
"username":"jaque",
|
||||
"blocked_at":null,
|
||||
"plain_password":"azerty",
|
||||
"created_at":null,
|
||||
"updated_at":null
|
||||
},
|
||||
"created_at":null
|
||||
},
|
||||
"created_at":null
|
||||
}
|
||||
],
|
||||
"created_by":{
|
||||
"id":"18902d22-245d-4d44-b23d-9f0e82688612",
|
||||
"name":{
|
||||
"first_name":"Jaque",
|
||||
"last_name":"Bono",
|
||||
"civility":null
|
||||
},
|
||||
"email": "jaque.bono@gmail.com",
|
||||
"birthday":"2019-08-07T20:34:08.013Z",
|
||||
"user_id":null,
|
||||
"vote_anonymous":null,
|
||||
"follow_anonymous":null,
|
||||
"user":{
|
||||
"id":"257abe9f-be17-4ad3-ae6a-b1dc9706d5d7",
|
||||
"username":"jaque",
|
||||
"plain_password":"azerty"
|
||||
}
|
||||
},
|
||||
"created_at":null,
|
||||
"version_id":"3311a7af-2a62-4e31-b4cd-889f8ead9737",
|
||||
"version_number":null
|
||||
}""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun `test Constitution serialize`() {
|
||||
val user = User(username = "jaque", plainPassword = "azerty")
|
||||
val citizen = CitizenBasic(
|
||||
name = CitizenI.Name("Jaque", "Bono"),
|
||||
email = "jaque.bono@gmail.com",
|
||||
birthday = DateTime.now(),
|
||||
user = user
|
||||
)
|
||||
val title1 = Constitution.Title(
|
||||
name = "plop"
|
||||
)
|
||||
val constitution = Constitution(
|
||||
title = "Hello world!",
|
||||
anonymous = true,
|
||||
titles = mutableListOf(title1),
|
||||
createdBy = citizen
|
||||
)
|
||||
println(constitution.serialize())
|
||||
constitution.serialize().contains("""Hello world!""") shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test Constitution Deserialize`() {
|
||||
val constitution2: Constitution = constitutionJson.deserialize()!!
|
||||
constitution2.id.toString() `should be equal to` "15814bb6-8d90-4c6a-a456-c3939a8ec75e"
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import fr.dcproject.entity.*
|
||||
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.`should equal`
|
||||
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 FollowTest {
|
||||
@Language("JSON")
|
||||
private val followJson: String = """{
|
||||
"id":"bae81585-d985-4d7a-9b58-3a13e911688a",
|
||||
"created_by":{
|
||||
"id":"4a87ad24-187a-46a8-97ab-00b30a24e561",
|
||||
"name":{
|
||||
"first_name":"Jaque",
|
||||
"last_name":"Bono",
|
||||
"civility":null
|
||||
},
|
||||
"email": "jaque.bono@gmail.com",
|
||||
"birthday":"2019-08-09T11:42:47.168Z",
|
||||
"user_id":null,
|
||||
"vote_anonymous":null,
|
||||
"follow_anonymous":null,
|
||||
"user":{
|
||||
"id":"721db690-d050-46e6-92b0-056f2e8ba993",
|
||||
"username":"jaque",
|
||||
"blocked_at":null,
|
||||
"plain_password":"azerty",
|
||||
"created_at":"2019-08-09T11:42:47.168Z",
|
||||
"updated_at":"2019-08-09T11:42:47.168Z"
|
||||
},
|
||||
"created_at":"2019-08-09T11:42:47.168Z"
|
||||
},
|
||||
"target":{
|
||||
"id":"34588ea7-c180-4694-801b-1b5c5a6ed73f",
|
||||
"title":"Hello world!",
|
||||
"anonymous":true,
|
||||
"content":"bla bla bla",
|
||||
"description":"this is the changement !",
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"created_by":{
|
||||
"id":"4a87ad24-187a-46a8-97ab-00b30a24e561",
|
||||
"name":{
|
||||
"first_name":"Jaque",
|
||||
"last_name":"Bono",
|
||||
"civility":null
|
||||
},
|
||||
"email": "jaque.bono@gmail.com",
|
||||
"birthday":"2019-08-09T11:42:47.168Z",
|
||||
"user_id":null,
|
||||
"vote_anonymous":null,
|
||||
"follow_anonymous":null,
|
||||
"user":{
|
||||
"id":"721db690-d050-46e6-92b0-056f2e8ba993",
|
||||
"username":"jaque",
|
||||
"blocked_at":null,
|
||||
"plain_password":"azerty",
|
||||
"created_at":"2019-08-09T11:42:47.168Z",
|
||||
"updated_at":"2019-08-09T11:42:47.168Z"
|
||||
}
|
||||
},
|
||||
"version_id":"a4aa7dd4-d174-42d2-9ba5-ae6f1129ffce",
|
||||
"version_number":null,
|
||||
"created_at":null
|
||||
},
|
||||
"created_at":"2019-08-09T11:42:47.168Z"
|
||||
}""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun `test Follow Article serialize`() {
|
||||
val user = User(username = "jaque", plainPassword = "azerty")
|
||||
val citizen = CitizenBasic(
|
||||
name = CitizenI.Name("Jaque", "Bono"),
|
||||
email = "jaque.bono@gmail.com",
|
||||
birthday = DateTime.now(),
|
||||
user = user
|
||||
)
|
||||
val article = Article(
|
||||
title = "Hello world!",
|
||||
content = "bla bla bla",
|
||||
description = "this is the changement !",
|
||||
createdBy = citizen
|
||||
)
|
||||
val follow = Follow(
|
||||
createdBy = citizen,
|
||||
target = article
|
||||
)
|
||||
follow.serialize().contains("""Hello world!""") shouldBe true
|
||||
println(follow.serialize())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test Follow Article Deserialize`() {
|
||||
val follow: Follow<ArticleSimple> = followJson.deserialize()!!
|
||||
follow.id.toString() `should be equal to` "bae81585-d985-4d7a-9b58-3a13e911688a"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import fr.dcproject.Env
|
||||
import fr.dcproject.entity.ArticleRefVersioning
|
||||
import fr.dcproject.component.article.ArticleRefVersioning
|
||||
import fr.dcproject.component.article.ArticleViewManager
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.module
|
||||
import fr.dcproject.views.ArticleViewManager
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.server.testing.withTestApplication
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.server.testing.*
|
||||
import io.ktor.util.*
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import fr.dcproject.entity.*
|
||||
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.`should equal`
|
||||
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 VoteTest {
|
||||
@Language("JSON")
|
||||
private val voteJson: String = """
|
||||
{
|
||||
"id": "032acc3d-e8c5-4cb2-9297-bec913ff8d9b",
|
||||
"created_by": {
|
||||
"id": "40c65a43-f9f8-45cd-aa31-953376e7c94a",
|
||||
"name": {
|
||||
"first_name": "Jaque",
|
||||
"last_name": "Bono",
|
||||
"civility": null
|
||||
},
|
||||
"email": "jaque.bono@gmail.com",
|
||||
"birthday": "2019-10-01T10:59:40.570Z",
|
||||
"user_id": null,
|
||||
"vote_anonymous": true,
|
||||
"follow_anonymous": true,
|
||||
"user": {
|
||||
"id": "f68df389-fb0d-423e-90fd-a140a9ed29b9",
|
||||
"username": "jaque",
|
||||
"blocked_at": null,
|
||||
"plain_password": "azerty",
|
||||
"roles": [],
|
||||
"created_at": "2019-10-01T10:59:40.570Z",
|
||||
"updated_at": "2019-10-01T10:59:40.570Z"
|
||||
},
|
||||
"deleted": false,
|
||||
"created_at": "2019-10-01T10:59:40.570Z",
|
||||
"updated_at": "2019-10-01T10:59:40.570Z",
|
||||
"deleted_at": null
|
||||
},
|
||||
"target": {
|
||||
"id": "90f28912-7bd5-4f37-a0ea-8620e3817d51",
|
||||
"title": "Hello world!",
|
||||
"anonymous": true,
|
||||
"content": "bla bla bla",
|
||||
"description": "this is the changement !",
|
||||
"tags": [],
|
||||
"draft": false,
|
||||
"last_version": false,
|
||||
"created_by": {
|
||||
"id": "40c65a43-f9f8-45cd-aa31-953376e7c94a",
|
||||
"name": {
|
||||
"first_name": "Jaque",
|
||||
"last_name": "Bono",
|
||||
"civility": null
|
||||
},
|
||||
"email": "jaque.bono@gmail.com",
|
||||
"birthday": "2019-10-01T10:59:40.570Z",
|
||||
"user_id": null,
|
||||
"vote_anonymous": true,
|
||||
"follow_anonymous": true,
|
||||
"user": {
|
||||
"id": "f68df389-fb0d-423e-90fd-a140a9ed29b9",
|
||||
"username": "jaque",
|
||||
"blocked_at": null,
|
||||
"plain_password": "azerty",
|
||||
"roles": [],
|
||||
"created_at": "2019-08-09T11:42:47.168Z",
|
||||
"updated_at": "2019-08-09T11:42:47.168Z"
|
||||
},
|
||||
"deleted": false,
|
||||
"created_at": "2019-08-09T11:42:47.168Z",
|
||||
"deleted_at": "2019-08-09T11:42:47.168Z"
|
||||
},
|
||||
"votes": {
|
||||
"up": 0,
|
||||
"neutral": 0,
|
||||
"down": 0,
|
||||
"updated_at": null
|
||||
},
|
||||
"version_id": "48dad61e-c54b-4f4c-9f66-428f90b94045",
|
||||
"version_number": null,
|
||||
"deleted": false,
|
||||
"created_at": "2019-10-01T10:59:40.570Z",
|
||||
"deleted_at": "2019-10-01T10:59:40.570Z"
|
||||
},
|
||||
"note": -1,
|
||||
"anonymous": true,
|
||||
"updated_at": "2019-10-01T10:59:40.570Z",
|
||||
"created_at": "2019-10-01T10:59:40.570Z"
|
||||
}""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun `test Vote Article serialize`() {
|
||||
val user = User(username = "jaque", plainPassword = "azerty")
|
||||
val citizen = CitizenBasic(
|
||||
name = CitizenI.Name("Jaque", "Bono"),
|
||||
email = "jaque.bono@gmail.com",
|
||||
birthday = DateTime.now(),
|
||||
user = user
|
||||
)
|
||||
val article = Article(
|
||||
title = "Hello world!",
|
||||
content = "bla bla bla",
|
||||
description = "this is the changement !",
|
||||
createdBy = citizen
|
||||
)
|
||||
val vote = Vote(
|
||||
createdBy = citizen,
|
||||
target = article,
|
||||
note = -1
|
||||
)
|
||||
vote.serialize().contains("""Hello world!""") shouldBe true
|
||||
vote.serialize().contains("-1") shouldBe true
|
||||
println(vote.serialize())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test Vote Article Deserialize`() {
|
||||
val vote: Vote<Article> = voteJson.deserialize()!!
|
||||
vote.id.toString() `should be equal to` "032acc3d-e8c5-4cb2-9297-bec913ff8d9b"
|
||||
vote.note.toString() `should be equal to` "-1"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package feature
|
||||
|
||||
import fr.dcproject.component.article.ArticleForUpdate
|
||||
import fr.dcproject.component.article.ArticleForView
|
||||
import fr.dcproject.component.article.ArticleRepository
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.repository.CommentArticle
|
||||
import fr.dcproject.utils.toUUID
|
||||
@@ -9,10 +12,7 @@ import org.joda.time.DateTime
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.get
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.entity.User as UserEntity
|
||||
import fr.dcproject.repository.Article as ArticleRepository
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
|
||||
class ArticleSteps : En, KoinTest {
|
||||
@@ -88,7 +88,7 @@ class ArticleSteps : En, KoinTest {
|
||||
("$firstName-$lastName".toLowerCase()).toLowerCase().replace(' ', '-')
|
||||
) ?: error("Citizen not exist")
|
||||
|
||||
val comment: CommentEntity<ArticleEntity> = CommentEntity(
|
||||
val comment: CommentForUpdate<ArticleForView, Citizen> = CommentForUpdate(
|
||||
id = id ?: params?.get("id")?.let { UUID.fromString(it) } ?: UUID.randomUUID(),
|
||||
createdBy = citizen,
|
||||
target = article,
|
||||
|
||||
@@ -26,13 +26,16 @@ class CitizenSteps : En, KoinTest {
|
||||
createCitizen(firstName, lastName)
|
||||
}
|
||||
|
||||
Given("I have citizen {word} {word} with") { firstName: String, lastName: String, extraData: DataTable? ->
|
||||
createCitizen(firstName, lastName, extraData)
|
||||
}
|
||||
|
||||
Given("I have citizen {word} {word} with ID {string}") { firstName: String, lastName: String, id: String ->
|
||||
createCitizen(firstName, lastName, id = UUID.fromString(id))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCitizen(firstName: String, lastName: String, extraData: DataTable? = null, id: UUID? = null) {
|
||||
|
||||
val params = extraData?.asMap<String, String>(String::class.java, String::class.java)
|
||||
val id: UUID = id ?: params?.get("id")?.let { UUID.fromString(it) } ?: UUID.randomUUID()
|
||||
val email = params?.get("email") ?: ("$firstName-$lastName".toLowerCase()) + "@dc-project.fr"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package feature
|
||||
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.repository.CommentConstitution
|
||||
import fr.dcproject.utils.toUUID
|
||||
@@ -9,7 +10,6 @@ import org.joda.time.DateTime
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.get
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.entity.User as UserEntity
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
import fr.dcproject.repository.Constitution as ConstitutionRepository
|
||||
@@ -66,7 +66,7 @@ class ConstitutionSteps : En, KoinTest {
|
||||
name = "My Title"
|
||||
)
|
||||
|
||||
val constitution = ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>>(
|
||||
val constitution = ConstitutionSimple<CitizenWithUserI, ConstitutionSimple.TitleSimple<ArticleRef>>(
|
||||
id = id ?: params?.get("id")?.toUUID() ?: UUID.randomUUID(),
|
||||
title = "hello",
|
||||
titles = mutableListOf(title1),
|
||||
@@ -91,7 +91,7 @@ class ConstitutionSteps : En, KoinTest {
|
||||
("$firstName-$lastName".toLowerCase()).toLowerCase().replace(' ', '-')
|
||||
) ?: error("Citizen not exist")
|
||||
|
||||
val comment: CommentEntity<ConstitutionRef> = CommentEntity(
|
||||
val comment: CommentForUpdate<ConstitutionRef, Citizen> = CommentForUpdate(
|
||||
id = params?.get("id")?.let { UUID.fromString(it) } ?: UUID.randomUUID(),
|
||||
createdBy = citizen,
|
||||
target = constitution,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package feature
|
||||
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.component.article.ArticleRef
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.entity.Follow
|
||||
import fr.dcproject.entity.FollowForUpdate
|
||||
import fr.dcproject.utils.toUUID
|
||||
import io.cucumber.java8.En
|
||||
import org.koin.test.KoinTest
|
||||
@@ -16,13 +16,13 @@ class FollowSteps : En, KoinTest {
|
||||
Given("I have follow of {word} {word} on article {string}") { firstName: String, lastName: String, articleId: String ->
|
||||
val username = "$firstName-$lastName".toLowerCase()
|
||||
val citizen = get<CitizenRepository>().findByUsername(username) ?: error("Citizen not exist")
|
||||
val follow = Follow(createdBy = citizen, target = ArticleRef(articleId.toUUID()))
|
||||
val follow = FollowForUpdate(createdBy = citizen, target = ArticleRef(articleId.toUUID()))
|
||||
get<FollowArticleRepository>().follow(follow)
|
||||
}
|
||||
Given("I have follow of {word} {word} on constitution {string}") { firstName: String, lastName: String, constitutionId: String ->
|
||||
val username = "$firstName-$lastName".toLowerCase()
|
||||
val citizen = get<CitizenRepository>().findByUsername(username) ?: error("Citizen not exist")
|
||||
val follow = Follow(createdBy = citizen, target = ConstitutionRef(constitutionId.toUUID()))
|
||||
val follow = FollowForUpdate(createdBy = citizen, target = ConstitutionRef(constitutionId.toUUID()))
|
||||
get<FollowConstitutionRepository>().follow(follow)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package feature
|
||||
import com.auth0.jwt.JWT
|
||||
import fr.dcproject.JwtConfig
|
||||
import io.cucumber.java8.En
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.*
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.get
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user