Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#21 Users get/post and promotions #40

Merged
merged 6 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ru.itmo.lms.botalka.api.http.endpoint

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import ru.itmo.lms.botalka.api.http.PromotionRequestDraftMessage
import ru.itmo.lms.botalka.api.http.PromotionRequestMessage
import ru.itmo.lms.botalka.api.http.PromotionRequestPatchMessage
import ru.itmo.lms.botalka.api.http.apis.PromotionApi
import ru.itmo.lms.botalka.api.http.error.InvalidPromotionRequestStatus
import ru.itmo.lms.botalka.api.http.message.toMessage
import ru.itmo.lms.botalka.api.http.message.toModel
import ru.itmo.lms.botalka.domain.model.PromotionRequest
import ru.itmo.lms.botalka.domain.model.User
import ru.itmo.lms.botalka.logic.PromotionService

@RestController
class PromotionHttpApi(
@Autowired private val promotionService: PromotionService,
) : PromotionApi {
override suspend fun promotionRequestIdPatch(
id: Int,
promotionRequestPatchMessage: PromotionRequestPatchMessage,
): ResponseEntity<Unit> {
val requestId = PromotionRequest.Id(id)

when (val status = promotionRequestPatchMessage.status.toModel()) {
PromotionRequest.Status.CREATED -> {
throw InvalidPromotionRequestStatus(
"""
Can't change promotion request status
to ${status.toMessage()}
""".trimIndent().replace("\n", " "),
)
}

PromotionRequest.Status.REJECTED -> {
promotionService.reject(requestId)
}

PromotionRequest.Status.APPROVED -> {
promotionService.approve(requestId)
}
}

return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
}

override suspend fun promotionRequestPost(
userId: Int,
promotionRequestDraftMessage: PromotionRequestDraftMessage,
): ResponseEntity<PromotionRequestMessage> {
val user = User.Id(userId)
val role = promotionRequestDraftMessage.role.toModel()
val draft = PromotionRequest.Draft(user, role)
val request = promotionService.request(draft)
return ResponseEntity.ok(request.toMessage())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ru.itmo.lms.botalka.api.http.endpoint

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import ru.itmo.lms.botalka.api.http.UserDraftMessage
import ru.itmo.lms.botalka.api.http.UserMessage
import ru.itmo.lms.botalka.api.http.apis.UserApi
import ru.itmo.lms.botalka.api.http.message.toMessage
import ru.itmo.lms.botalka.api.http.message.toModel
import ru.itmo.lms.botalka.domain.model.User
import ru.itmo.lms.botalka.logic.UserService

@RestController
class UserHttpApi(
@Autowired private val userService: UserService,
) : UserApi {
override suspend fun userIdGet(id: Int): ResponseEntity<UserMessage> {
val userId = User.Id(id)
val user = userService.getById(userId)
return ResponseEntity.ok(user.toMessage())
}

override suspend fun userPost(
userDraftMessage: UserDraftMessage,
): ResponseEntity<UserMessage> {
val draft = userDraftMessage.toModel()
val user = userService.create(draft)
return ResponseEntity.ok(user.toMessage())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ru.itmo.lms.botalka.api.http.error

class InvalidPromotionRequestStatus(message: String) :
Exception("Invalid promotion request status: $message")
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ru.itmo.lms.botalka.api.http.message

import ru.itmo.lms.botalka.api.http.PromotionRequestMessage
import ru.itmo.lms.botalka.api.http.PromotionRequestStatusMessage
import ru.itmo.lms.botalka.domain.model.PromotionRequest

fun PromotionRequest.toMessage(): PromotionRequestMessage =
PromotionRequestMessage(
id = this.id.number,
userId = this.user.number,
role = this.role.toMessage(),
status = this.status.toMessage(),
)

fun PromotionRequest.Status.toMessage(): PromotionRequestStatusMessage =
when (this) {
PromotionRequest.Status.CREATED -> {
PromotionRequestStatusMessage.NEW
}

PromotionRequest.Status.REJECTED -> {
PromotionRequestStatusMessage.CANCELED
}

PromotionRequest.Status.APPROVED -> {
PromotionRequestStatusMessage.APPROVED
}
}

fun PromotionRequestStatusMessage.toModel(): PromotionRequest.Status =
when (this) {
PromotionRequestStatusMessage.NEW -> {
PromotionRequest.Status.CREATED
}

PromotionRequestStatusMessage.APPROVED -> {
PromotionRequest.Status.APPROVED
}

PromotionRequestStatusMessage.CANCELED -> {
PromotionRequest.Status.REJECTED
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ru.itmo.lms.botalka.api.http.message

import ru.itmo.lms.botalka.api.http.UserDraftMessage
import ru.itmo.lms.botalka.api.http.UserMessage
import ru.itmo.lms.botalka.api.http.UserRoleMessage
import ru.itmo.lms.botalka.domain.model.User

fun User.Role.toMessage(): UserRoleMessage =
when (this) {
User.Role.TEACHER -> UserRoleMessage.TEACHER
User.Role.STUDENT -> UserRoleMessage.STUDENT
}

fun UserRoleMessage.toModel(): User.Role =
when (this) {
UserRoleMessage.STUDENT -> User.Role.STUDENT
UserRoleMessage.TEACHER -> User.Role.TEACHER
}

fun User.toMessage(): UserMessage =
UserMessage(
id = this.id.number,
alias = this.alias.text,
roles = this.roles.map { it.toMessage() },
)

fun User.Draft.toMessage(): UserDraftMessage =
UserDraftMessage(
alias = this.alias.text,
)

fun UserDraftMessage.toModel(): User.Draft =
User.Draft(
alias = User.Alias(this.alias),
)

fun UserMessage.toModel(): User =
User(
id = User.Id(this.id),
alias = User.Alias(this.alias),
roles = this.roles.map { it.toModel() }.toSet(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package ru.itmo.lms.botalka.commons

import org.apache.commons.lang3.StringUtils

fun String.abbreviated(maxLength: Int = 8): String =
fun String.abbreviated(maxLength: Int = 16): String =
StringUtils.abbreviate(this, maxLength)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.itmo.lms.botalka.commons

fun requireId(number: Int) {
require(0 < number) {
"Unique id must be a positive, got $number"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ru.itmo.lms.botalka.domain.exception

open class DomainException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.itmo.lms.botalka.domain.exception

import ru.itmo.lms.botalka.domain.model.PromotionRequest

class PromotionRequestResolvedException(id: PromotionRequest.Id) :
DomainException("Promotion request with id $id was already resolved")
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ru.itmo.lms.botalka.domain.model

import ru.itmo.lms.botalka.commons.abbreviated
import ru.itmo.lms.botalka.commons.requireId
import java.time.OffsetDateTime

data class Homework(
Expand All @@ -14,11 +15,7 @@ data class Homework(
@JvmInline
value class Id(val number: Int) {
init {
require(0 < number) {
"""
Unique id must be a positive, got $number
""".trimIndent()
}
requireId(number)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ru.itmo.lms.botalka.domain.model

import ru.itmo.lms.botalka.commons.requireId

data class PromotionRequest(
val id: Id,
val user: User.Id,
val role: User.Role,
val status: Status,
) {
@JvmInline
value class Id(val number: Int) {
init {
requireId(number)
}
}

enum class Status {
CREATED,
REJECTED,
APPROVED,
}

data class Draft(
val user: User.Id,
val role: User.Role,
)

val isResolved: Boolean
get() = status != Status.CREATED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ru.itmo.lms.botalka.domain.model

data class Student(val id: User.Id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ru.itmo.lms.botalka.domain.model

data class Teacher(val id: User.Id)
49 changes: 49 additions & 0 deletions botalka/src/main/kotlin/ru/itmo/lms/botalka/domain/model/User.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package ru.itmo.lms.botalka.domain.model

import ru.itmo.lms.botalka.commons.abbreviated
import ru.itmo.lms.botalka.commons.requireId

data class User(
val id: Id,
val alias: Alias,
val roles: Set<Role>,
) {
@JvmInline
value class Id(val number: Int) {
init {
requireId(number)
}
}

@JvmInline
value class Alias(val text: String) {
init {
require(text.length in lengthRange) {
"""
User alias length must be in range $lengthRange,
but got ${text.abbreviated()}
""".trimIndent()
}
require(!regex.matches(text)) {
"""
User alias must contain only latin letters,
but got ${text.abbreviated()}
""".trimIndent()
}
}

companion object {
private val regex = Regex.fromLiteral("[a-zA-Z]{3,31}")
private val lengthRange = 3..31
}
}

enum class Role {
TEACHER,
STUDENT,
}

data class Draft(
val alias: Alias,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.itmo.lms.botalka.logic

import ru.itmo.lms.botalka.domain.model.PromotionRequest

interface PromotionService {
suspend fun request(promotion: PromotionRequest.Draft): PromotionRequest
suspend fun approve(id: PromotionRequest.Id)
suspend fun reject(id: PromotionRequest.Id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.itmo.lms.botalka.logic

import ru.itmo.lms.botalka.domain.model.User

interface UserService {
suspend fun getById(id: User.Id): User
suspend fun create(user: User.Draft): User
suspend fun promote(id: User.Id, role: User.Role)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ru.itmo.lms.botalka.logic.basic

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import ru.itmo.lms.botalka.domain.exception.PromotionRequestResolvedException
import ru.itmo.lms.botalka.domain.model.PromotionRequest
import ru.itmo.lms.botalka.logic.PromotionService
import ru.itmo.lms.botalka.logic.UserService
import ru.itmo.lms.botalka.storage.PromotionStorage

@Service
class BasicPromotionService(
@Autowired private val storage: PromotionStorage,
@Autowired private val userService: UserService,
) : PromotionService {
override suspend fun request(
promotion: PromotionRequest.Draft,
): PromotionRequest =
storage.create(promotion)

override suspend fun approve(id: PromotionRequest.Id) {
val request = storage.getById(id)
if (request.isResolved) {
throw PromotionRequestResolvedException(request.id)
}
storage.updateStatus(request.id, PromotionRequest.Status.APPROVED)
userService.promote(request.user, request.role)
}

override suspend fun reject(id: PromotionRequest.Id) {
if (storage.getById(id).isResolved) {
throw PromotionRequestResolvedException(id)
}
storage.updateStatus(id, PromotionRequest.Status.REJECTED)
}
}
Loading
Loading