Move vote to component

This commit is contained in:
2021-01-22 21:45:02 +01:00
parent c92d0b5640
commit c1b8b508ac
29 changed files with 263 additions and 187 deletions

View File

@@ -9,6 +9,8 @@ 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.vote.entity.Votable
import fr.dcproject.component.vote.entity.VotableImp
import fr.dcproject.component.workgroup.WorkgroupCart
import fr.dcproject.component.workgroup.WorkgroupCartI
import fr.dcproject.component.workgroup.WorkgroupRef
@@ -19,8 +21,6 @@ import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.dcproject.entity.VersionableRef
import fr.dcproject.entity.VersionableRefImp
import fr.dcproject.entity.Votable
import fr.dcproject.entity.VotableImp
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityDeletedAt

View File

@@ -7,10 +7,10 @@ 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.component.vote.dto.Votable
import fr.dcproject.dto.CreatedAt
import fr.dcproject.dto.Versionable
import fr.dcproject.dto.Viewable
import fr.dcproject.dto.Votable
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.features.NotFoundException

View File

@@ -1,13 +1,13 @@
package fr.dcproject.component.comment.generic
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.vote.entity.Votable
import fr.dcproject.component.vote.entity.VotableImp
import fr.dcproject.entity.EntityI
import fr.dcproject.entity.ExtraI
import fr.dcproject.entity.HasTarget
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.dcproject.entity.Votable
import fr.dcproject.entity.VotableImp
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityCreatedBy

View File

@@ -1,4 +1,4 @@
package fr.dcproject.repository
package fr.dcproject.component.opinion
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleRef

View File

@@ -15,7 +15,7 @@ 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
import fr.dcproject.component.opinion.OpinionRepositoryArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object GetCitizenOpinions {

View File

@@ -13,7 +13,7 @@ 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
import fr.dcproject.component.opinion.OpinionRepositoryArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object GetMyOpinionsArticle {

View File

@@ -1,6 +1,7 @@
package fr.dcproject.component.opinion.routes
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.opinion.OpinionChoiceRepository
import fr.dcproject.component.opinion.OpinionChoiceVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
@@ -9,7 +10,6 @@ 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 {

View File

@@ -17,7 +17,7 @@ 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
import fr.dcproject.component.opinion.OpinionRepositoryArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI
object OpinionArticle {

View File

@@ -0,0 +1,132 @@
package fr.dcproject.component.vote
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.comment.generic.CommentForView
import fr.dcproject.component.vote.entity.VoteAggregation
import fr.dcproject.component.vote.entity.VoteForUpdateI
import fr.dcproject.entity.Constitution
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import java.util.UUID
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.component.vote.entity.Vote as VoteEntity
abstract class VoteRepositoryAbs<T : TargetI>(override var requester: Requester) : RepositoryI {
fun vote(vote: VoteForUpdateI<T, *>, anonymous: Boolean? = null): VoteAggregation {
val author = vote.createdBy
return requester
.getFunction("vote")
.selectOne(
"reference" to vote.target.reference,
"target_id" to vote.target.id,
"note" to vote.note,
"created_by_id" to author.id,
"anonymous" to anonymous
)!!
}
fun findByCitizen(
citizenId: UUID,
target: String,
typeReference: TypeReference<List<VoteEntity<T>>>,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<T>> {
return requester.run {
getFunction("find_votes_by_citizen")
.select(
page,
limit,
typeReference,
mapOf(
"created_by_id" to citizenId,
"reference" to target
)
)
}
}
fun findCitizenVotesByTargets(
citizen: CitizenEntity,
targets: List<UUID>
): List<VoteEntity<*>> {
val typeReference = object : TypeReference<List<VoteEntity<TargetRef>>>() {}
return requester.run {
getFunction("find_citizen_votes_by_target_ids")
.select(
typeReference,
mapOf(
"citizen_id" to citizen.id,
"ids" to targets
)
)
}
}
}
class VoteRepository(requester: Requester) : VoteRepositoryAbs<TargetRef>(requester)
class VoteArticleRepository(requester: Requester) : VoteRepositoryAbs<ArticleForView>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<ArticleForView>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<ArticleForView>>>() {},
page,
limit
)
}
class VoteArticleCommentRepository(requester: Requester) : VoteRepositoryAbs<CommentForView<ArticleForView, CitizenRef>>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<CommentForView<ArticleForView, CitizenRef>>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<CommentForView<ArticleForView, CitizenRef>>>>() {},
page,
limit
)
}
class VoteCommentRepository(requester: Requester) : VoteRepositoryAbs<CommentForView<TargetRef, CitizenRef>>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<CommentForView<TargetRef, CitizenRef>>> =
findByCitizen(
citizen.id,
"article",
object : TypeReference<List<VoteEntity<CommentForView<TargetRef, CitizenRef>>>>() {},
page,
limit
)
}
class VoteConstitutionRepository(requester: Requester) : VoteRepositoryAbs<Constitution>(requester) {
fun findByCitizen(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<VoteEntity<Constitution>> =
findByCitizen(
citizen.id,
"constitution",
object : TypeReference<List<VoteEntity<Constitution>>>() {},
page,
limit
)
}

View File

@@ -0,0 +1,26 @@
package fr.dcproject.component.vote
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.vote.entity.VoteForUpdateI
import fr.dcproject.entity.TargetI
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityDeletedAt
import fr.dcproject.component.vote.entity.Vote as VoteEntity
class VoteVoter : Voter() {
fun <S> canCreate(subject: VoteForUpdateI<S, *>, citizen: CitizenI?): VoterResponse where S : EntityDeletedAt, S : TargetI = when {
citizen == null -> denied("You must be connected for vote", "vote.create.connected")
subject.target.isDeleted() -> denied("You cannot vote on deleted target", "vote.create.isDeleted")
else -> granted()
}
fun <S : VoteEntity<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
fun canView(subject: VoteEntity<*>, citizen: CitizenI?): VoterResponse = when {
citizen == null -> denied("You must be connected for view your votes", "vote.view.connected")
subject.createdBy.id != citizen.id -> denied("You can only display your votes", "vote.view.onlyYours")
else -> granted()
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
package fr.dcproject.component.vote.entity
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.ExtraI
import fr.dcproject.entity.HasTarget
import fr.dcproject.entity.TargetI
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityCreatedByImp
import fr.postgresjson.entity.EntityUpdatedAt
import fr.postgresjson.entity.EntityUpdatedAtImp
import fr.postgresjson.entity.UuidEntityI
import java.util.UUID
@Deprecated("")
class Vote<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override val target: T,
var note: Int,
var anonymous: Boolean = true
) : ExtraI<T, CitizenBasicI>,
VoteRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp() {
init {
if (note > 1 && note < -1) {
error("note must be 1, 0 or -1")
}
}
}
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, HasTarget<T>, EntityCreatedBy<C> {
override val id: UUID
val note: Int
override val target: T
override val createdBy: C
}
open class VoteRef(
override val id: UUID
) : VoteI
interface VoteI : UuidEntityI

View File

@@ -0,0 +1,16 @@
package fr.dcproject.component.vote.entity
import fr.postgresjson.entity.EntityI
import fr.postgresjson.entity.EntityUpdatedAt
import fr.postgresjson.entity.EntityUpdatedAtImp
class VoteAggregation(
val up: Int,
val neutral: Int,
val down: Int,
val total: Int,
val score: Int
) : EntityI,
EntityUpdatedAt by EntityUpdatedAtImp() {
constructor() : this(0, 0, 0, 0, 0)
}

View File

@@ -0,0 +1,33 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.vote.VoteRepository
import fr.dcproject.component.vote.VoteVoter
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 java.util.UUID
@KtorExperimentalLocationsAPI
object GetCitizenVotes {
@Location("/citizens/{citizen}/votes")
class CitizenVotesRequest(val citizen: Citizen, id: List<String>) {
val id: List<UUID> = id.toUUID()
}
fun Route.getCitizenVote(repo: VoteRepository, voter: VoteVoter) {
get<CitizenVotesRequest> {
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
if (votes.isNotEmpty()) {
voter.assert { canView(votes, citizenOrNull) }
}
call.respond(votes)
}
}
}

View File

@@ -0,0 +1,35 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.vote.VoteArticleRepository
import fr.dcproject.component.vote.VoteVoter
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
@KtorExperimentalLocationsAPI
object GetCitizenVotesOnArticle {
@Location("/citizens/{citizen}/votes/articles")
class CitizenVoteArticleRequest(
val citizen: Citizen,
page: Int = 1,
limit: Int = 50,
val search: String? = null
) : PaginatedRequestI by PaginatedRequest(page, limit)
fun Route.getCitizenVotesOnArticle(repo: VoteArticleRepository, voter: VoteVoter) {
get<CitizenVoteArticleRequest> {
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
voter.assert { canView(votes.result, citizenOrNull) }
call.respond(votes)
}
}
}

View File

@@ -0,0 +1,39 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.vote.VoteArticleRepository
import fr.dcproject.component.vote.VoteVoter
import fr.dcproject.component.vote.entity.VoteForUpdate
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
@KtorExperimentalLocationsAPI
object PutVoteOnArticle {
@Location("/articles/{article}/vote")
class ArticleVoteRequest(val article: ArticleForView) {
data class Content(var note: Int)
}
fun Route.putVoteOnArticle(repo: VoteArticleRepository, voter: VoteVoter) {
put<ArticleVoteRequest> {
val content = call.receive<ArticleVoteRequest.Content>()
val vote = VoteForUpdate(
target = it.article,
note = content.note,
createdBy = this.citizen
)
voter.assert { canCreate(vote, citizenOrNull) }
val votes = repo.vote(vote)
call.respond(HttpStatusCode.Created, votes)
}
}
}

View File

@@ -0,0 +1,41 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.vote.VoteCommentRepository
import fr.dcproject.component.vote.VoteVoter
import fr.dcproject.component.vote.entity.VoteForUpdate
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
@KtorExperimentalLocationsAPI
object PutVoteOnComment {
@Location("/comments/{comment}/vote")
class CommentVoteRequest(val comment: UUID) {
data class Content(var note: Int)
}
fun Route.putVoteOnComment(voteCommentRepo: VoteCommentRepository, commentRepo: CommentRepository, voter: VoteVoter) {
put<CommentVoteRequest> {
val comment = commentRepo.findById(it.comment)!!
val content = call.receive<CommentVoteRequest.Content>()
val vote = VoteForUpdate(
target = comment,
note = content.note,
createdBy = this.citizen
)
voter.assert { canCreate(vote, citizenOrNull) }
val votes = voteCommentRepo.vote(vote)
call.respond(HttpStatusCode.Created, votes)
}
}
}

View File

@@ -0,0 +1,40 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.vote.VoteConstitutionRepository
import fr.dcproject.component.vote.VoteVoter
import fr.dcproject.component.vote.entity.VoteForUpdate
import fr.dcproject.component.vote.routes.VoteConstitution.ConstitutionVoteRequest.Input
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 fr.dcproject.entity.Constitution as ConstitutionEntity
@KtorExperimentalLocationsAPI
object VoteConstitution {
@Location("/constitutions/{constitution}/vote")
class ConstitutionVoteRequest(val constitution: ConstitutionEntity) {
data class Input(var note: Int)
}
fun Route.voteConstitution(repo: VoteConstitutionRepository, voter: VoteVoter) {
put<ConstitutionVoteRequest> {
val content = call.receive<Input>()
val vote = VoteForUpdate(
target = it.constitution,
note = content.note,
createdBy = this.citizen
)
voter.assert { canCreate(vote, citizenOrNull) }
repo.vote(vote)
call.respond(HttpStatusCode.Created)
}
}
}

View File

@@ -0,0 +1,22 @@
package fr.dcproject.component.vote.routes
import fr.dcproject.component.vote.routes.GetCitizenVotes.getCitizenVote
import fr.dcproject.component.vote.routes.GetCitizenVotesOnArticle.getCitizenVotesOnArticle
import fr.dcproject.component.vote.routes.PutVoteOnArticle.putVoteOnArticle
import fr.dcproject.component.vote.routes.PutVoteOnComment.putVoteOnComment
import fr.dcproject.component.vote.routes.VoteConstitution.voteConstitution
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installVoteRoutes() {
authenticate(optional = true) {
getCitizenVote(get(), get())
getCitizenVotesOnArticle(get(), get())
putVoteOnArticle(get(), get())
putVoteOnComment(get(), get(), get())
voteConstitution(get(), get())
}
}