Move opinions to component

This commit is contained in:
2021-01-22 21:11:04 +01:00
parent 73e96c0c46
commit c92d0b5640
24 changed files with 244 additions and 170 deletions

View File

@@ -6,15 +6,15 @@ import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenCartI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.opinion.entity.Opinionable
import fr.dcproject.component.opinion.entity.OpinionableImp
import fr.dcproject.component.opinion.entity.Opinions
import fr.dcproject.component.workgroup.WorkgroupCart
import fr.dcproject.component.workgroup.WorkgroupCartI
import fr.dcproject.component.workgroup.WorkgroupRef
import fr.dcproject.component.workgroup.WorkgroupSimple
import fr.dcproject.entity.CreatedBy
import fr.dcproject.entity.CreatedByImp
import fr.dcproject.entity.Opinionable
import fr.dcproject.entity.OpinionableImp
import fr.dcproject.entity.Opinions
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.dcproject.entity.VersionableRef

View File

@@ -6,8 +6,8 @@ import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.component.article.routes.GetOneArticle.ArticleRequest.Output
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.dto.Opinionable
import fr.dcproject.dto.CreatedAt
import fr.dcproject.dto.Opinionable
import fr.dcproject.dto.Versionable
import fr.dcproject.dto.Viewable
import fr.dcproject.dto.Votable

View File

@@ -0,0 +1,15 @@
package fr.dcproject.component.opinion
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.opinion.entity.OpinionChoice
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
class OpinionChoiceVoter : Voter() {
fun canView(subjects: List<OpinionChoice>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
fun canView(subject: OpinionChoice, citizen: CitizenI?): VoterResponse {
return granted()
}
}

View File

@@ -0,0 +1,160 @@
package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.opinion.entity.OpinionForUpdate
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import net.pearx.kasechange.toSnakeCase
import java.util.UUID
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.component.opinion.entity.Opinion as OpinionEntity
import fr.dcproject.component.opinion.entity.OpinionArticle as OpinionArticleEntity
import fr.dcproject.component.opinion.entity.OpinionChoice as OpinionChoiceEntity
open class OpinionChoiceRepository(override val requester: Requester) : RepositoryI {
/**
* find all opinion choices
* can be filtered by target compatibility
*/
fun findOpinionsChoices(targets: List<String> = emptyList()): List<OpinionChoiceEntity> =
requester
.getFunction("find_opinion_choices")
.select(
"targets" to targets
)
/**
* find opinion choices by name
*/
fun findOpinionsChoiceByName(name: String): OpinionChoiceEntity? =
findOpinionsChoices().first {
it.name == name
}
/**
* find one opinion choices by id
*/
fun findOpinionChoiceById(id: UUID): OpinionChoiceEntity? =
requester
.getFunction("find_opinion_choice_by_id")
.selectOne(
"id" to id
)
/**
* find one opinion choices by id
*/
fun findOpinionChoicesByIds(ids: List<UUID>): List<OpinionChoiceEntity> =
requester
.getFunction("find_opinion_choices_by_ids")
.select(
"ids" to ids
)
fun upsertOpinionChoice(opinionChoice: OpinionChoiceEntity): OpinionChoiceEntity = requester
.getFunction("upsert_opinion_choice")
.selectOne(
"resource" to opinionChoice
)!!
}
abstract class OpinionRepository<T : TargetRef>(requester: Requester) : OpinionChoiceRepository(requester) {
/**
* Create an Opinion on target (article,...)
*/
abstract fun updateOpinions(opinions: List<OpinionForUpdate<*>>): List<OpinionEntity<T>>
fun updateOpinions(opinion: OpinionForUpdate<*>): List<OpinionEntity<T>> =
updateOpinions(listOf(opinion))
abstract fun addOpinion(opinion: OpinionForUpdate<T>): OpinionEntity<T>
/**
* Find opinions of one citizen filtered by target ids
*/
fun findCitizenOpinionsByTargets(
citizen: CitizenEntity,
targets: List<UUID>
): List<OpinionEntity<T>> {
val typeReference = object : TypeReference<List<OpinionEntity<T>>>() {}
return requester.run {
getFunction("find_citizen_opinions_by_target_ids")
.select(
typeReference,
mapOf(
"citizen_id" to citizen.id,
"ids" to targets
)
)
}
}
/**
* find opinion of citizen filtered by one target id
*/
fun findCitizenOpinionsByTarget(
citizen: CitizenEntity,
target: UUID
): List<OpinionEntity<T>> {
val typeReference = object : TypeReference<List<OpinionEntity<T>>>() {}
return requester
.getFunction("find_citizen_opinions_by_target_id")
.select(
typeReference,
mapOf(
"citizen_id" to citizen.id,
"id" to target
)
)
}
/**
* find paginated opinion of one citizen
* can be sorted
*/
fun findCitizenOpinions(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: RepositoryI.Direction? = null
): Paginated<OpinionEntity<TargetRef>> {
return requester
.getFunction("find_citizen_opinions")
.select(
page,
limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"citizen_id" to citizen.id
)
}
}
class OpinionRepositoryArticle(requester: Requester) : OpinionRepository<ArticleRef>(requester) {
/**
* Update Opinions on Article (Delete old one)
*/
override fun updateOpinions(opinions: List<OpinionForUpdate<*>>): List<OpinionArticleEntity> {
return requester
/* TODO change SQL function to not use .first() and pass all createdBy and target */
.getFunction("update_citizen_opinions_by_target_id")
.select(
"choices_ids" to opinions.map { it.choice.id },
"citizen_id" to opinions.first().createdBy.id,
"target_id" to opinions.first().target.id,
"target_reference" to opinions.first().target.reference
)
}
/**
* Add Opinions on Article
*/
override fun addOpinion(opinion: OpinionForUpdate<ArticleRef>): OpinionArticleEntity {
return requester
.getFunction("upsert_opinion")
.selectOne("resource" to opinion)!!
}
}

View File

@@ -0,0 +1,35 @@
package fr.dcproject.component.opinion
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.opinion.entity.OpinionI
import fr.dcproject.entity.HasTarget
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityDeletedAt
class OpinionVoter : Voter() {
fun <S> canCreate(subjects: List<S>, citizen: CitizenI?): VoterResponse where S : OpinionI, S : HasTarget<*> =
canAll(subjects) { canCreate(it, citizen) }
fun <S> canCreate(subject: S, citizen: CitizenI?): VoterResponse where S : OpinionI, S : HasTarget<*> {
val target = subject.target
return when {
citizen == null -> denied("You must be connected to make an opinion", "opinion.create.notConnected")
target is EntityDeletedAt && target.isDeleted() -> denied("You cannot make opinion on deleted target", "opinion.create.deletedTarget")
else -> granted()
}
}
fun <S : OpinionI, SS : List<S>> canView(subjects: SS, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
fun <S : OpinionI> canView(subject: S, citizen: CitizenI?): VoterResponse = granted()
fun <S, C : CitizenI> canDelete(subject: S, citizen: CitizenI?): VoterResponse where S : EntityCreatedBy<C>, S : OpinionI = when {
citizen == null -> denied("You must be connected to delete opinion", "opinion.delete.notConnected")
subject.createdBy.id != citizen.id -> denied("You can only delete your opinions", "opinion.delete.notYours")
else -> granted()
}
}

View File

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

View File

@@ -0,0 +1,54 @@
package fr.dcproject.component.opinion.entity
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.ExtraI
import fr.dcproject.entity.HasTarget
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityCreatedByImp
import fr.postgresjson.entity.UuidEntityI
import java.util.UUID
@Deprecated("")
open class Opinion<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override val target: T,
val choice: OpinionChoice
) : OpinionRef(id),
ExtraI<T, CitizenBasicI>,
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
fun getName(): String = choice.name
}
@Deprecated("")
class OpinionArticle(
id: UUID = UUID.randomUUID(),
createdBy: CitizenBasic,
target: ArticleRef,
choice: OpinionChoice
) : Opinion<ArticleRef>(id, createdBy, target, choice)
data class OpinionForUpdate<T : TargetI>(
override val id: UUID = UUID.randomUUID(),
override val target: T,
val choice: OpinionChoiceRef,
override val createdBy: CitizenRef
) : OpinionRef(id),
HasTarget<T>,
EntityCreatedBy<CitizenI> by EntityCreatedByImp(createdBy)
open class OpinionRef(
override val id: UUID
) : OpinionI, TargetRef(id)
interface OpinionI : UuidEntityI

View File

@@ -0,0 +1,20 @@
package fr.dcproject.component.opinion.entity
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityDeletedAt
import fr.postgresjson.entity.EntityDeletedAtImp
import fr.postgresjson.entity.UuidEntity
import java.util.UUID
class OpinionChoice(
id: UUID? = null,
val name: String,
val target: List<String>?
) : OpinionChoiceRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp()
open class OpinionChoiceRef(
id: UUID?
) : UuidEntity(id ?: UUID.randomUUID())

View File

@@ -0,0 +1,12 @@
package fr.dcproject.component.opinion.entity
typealias Opinions = Map<String, Int>
typealias OpinionsMutable = MutableMap<String, Int>
interface Opinionable {
val opinions: Opinions
}
class OpinionableImp : Opinionable {
override var opinions: OpinionsMutable = mutableMapOf()
}

View File

@@ -0,0 +1,38 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.OpinionVoter
import fr.dcproject.component.opinion.entity.Opinion
import fr.dcproject.utils.toUUID
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.UUID
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.repository.OpinionRepositoryArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object GetCitizenOpinions {
/**
* Get all Opinion of citizen on targets by target ids
*/
@Location("/citizens/{citizen}/opinions")
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>) : KoinComponent {
val id: List<UUID> = id.toUUID()
}
fun Route.getCitizenOpinions(repo: OpinionArticleRepository, voter: OpinionVoter) {
get<CitizenOpinions> {
val opinionsEntities: List<Opinion<ArticleRef>> = repo.findCitizenOpinionsByTargets(it.citizen, it.id)
voter.assert { canView(opinionsEntities, citizenOrNull) }
call.respond(opinionsEntities)
}
}
}

View File

@@ -0,0 +1,37 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.opinion.OpinionVoter
import fr.dcproject.routes.PaginatedRequest
import fr.dcproject.routes.PaginatedRequestI
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.repository.OpinionRepositoryArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object GetMyOpinionsArticle {
/**
* Get paginated opinions of citizen for all articles
*/
@Location("/citizens/{citizen}/opinions/articles")
class CitizenOpinionsArticleRequest(
val citizen: CitizenRef,
page: Int = 1,
limit: Int = 50
) : PaginatedRequestI by PaginatedRequest(page, limit)
fun Route.getMyOpinionsArticle(repo: OpinionArticleRepository, voter: OpinionVoter) {
get<CitizenOpinionsArticleRequest> {
val opinions = repo.findCitizenOpinions(citizen, it.page, it.limit)
voter.assert { canView(opinions.result, citizenOrNull) }
call.respond(opinions)
}
}
}

View File

@@ -0,0 +1,26 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.OpinionChoiceVoter
import fr.dcproject.component.opinion.entity.OpinionChoice
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object GetOpinionChoice {
@Location("/opinions/{opinionChoice}")
class OpinionChoiceRequest(val opinionChoice: OpinionChoice)
fun Route.getOpinionChoice(voter: OpinionChoiceVoter) {
get<OpinionChoiceRequest> {
voter.assert { canView(it.opinionChoice, citizenOrNull) }
call.respond(it.opinionChoice)
}
}
}

View File

@@ -0,0 +1,27 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.OpinionChoiceVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
import fr.dcproject.repository.OpinionChoiceRepository as OpinionChoiceRepository
@KtorExperimentalLocationsAPI
object GetOpinionChoices {
@Location("/opinions")
class OpinionChoicesRequest(val targets: List<String> = emptyList())
fun Route.getOpinionChoices(repo: OpinionChoiceRepository, voter: OpinionChoiceVoter) {
get<OpinionChoicesRequest> {
val opinionChoices = repo.findOpinionsChoices(it.targets)
voter.assert { canView(opinionChoices, citizenOrNull) }
call.respond(opinionChoices)
}
}
}

View File

@@ -0,0 +1,50 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.OpinionVoter
import fr.dcproject.component.opinion.entity.OpinionChoiceRef
import fr.dcproject.component.opinion.entity.OpinionForUpdate
import fr.dcproject.utils.toUUID
import fr.dcproject.voter.assert
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 java.util.UUID
import fr.dcproject.repository.OpinionRepositoryArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object OpinionArticle {
/**
* Put an opinion on one article
*/
@Location("/articles/{article}/opinions")
class ArticleOpinion(val article: ArticleForView) {
class Body(ids: List<String>) {
val ids: List<UUID> = ids.map { it.toUUID() }
}
}
fun Route.setOpinionOnArticle(repo: OpinionArticleRepository, voter: OpinionVoter) {
put<ArticleOpinion> {
call.receive<ArticleOpinion.Body>().ids.map { id ->
OpinionForUpdate(
choice = OpinionChoiceRef(id),
target = it.article,
createdBy = citizen
)
}.let { opinions ->
voter.assert { canCreate(opinions, citizenOrNull) }
repo.updateOpinions(opinions)
}.let {
call.respond(HttpStatusCode.Created, it)
}
}
}
}

View File

@@ -0,0 +1,22 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.opinion.routes.GetCitizenOpinions.getCitizenOpinions
import fr.dcproject.component.opinion.routes.GetMyOpinionsArticle.getMyOpinionsArticle
import fr.dcproject.component.opinion.routes.GetOpinionChoice.getOpinionChoice
import fr.dcproject.component.opinion.routes.GetOpinionChoices.getOpinionChoices
import fr.dcproject.component.opinion.routes.OpinionArticle.setOpinionOnArticle
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installOpinionRoutes() {
authenticate(optional = true) {
getCitizenOpinions(get(), get())
getMyOpinionsArticle(get(), get())
setOpinionOnArticle(get(), get())
getOpinionChoice(get())
getOpinionChoices(get(), get())
}
}