Skip to content

Commit

Permalink
#63 Added homework deadline (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
vityaman authored Apr 20, 2024
1 parent 66f1396 commit 8fbfff7
Show file tree
Hide file tree
Showing 17 changed files with 194 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package ru.vityaman.lms.botalka
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import java.time.Clock

@SpringBootApplication(exclude = [JooqAutoConfiguration::class])
class BotalkaApplication
class BotalkaApplication {
@Bean
fun clock() = Clock.systemUTC()
}

fun main(args: Array<String>) {
runApplication<BotalkaApplication>(args = args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ru.vityaman.lms.botalka.api.http.error
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import ru.vityaman.lms.botalka.api.http.server.GeneralErrorMessage
import ru.vityaman.lms.botalka.domain.exception.DeadlinePassedException
import ru.vityaman.lms.botalka.domain.exception.DomainException
import ru.vityaman.lms.botalka.domain.exception.InvalidValueException
import ru.vityaman.lms.botalka.domain.exception.NotFoundException
Expand All @@ -13,12 +14,10 @@ val DomainException.httpCode: HttpStatus
is InvalidValueException -> HttpStatus.BAD_REQUEST
is NotFoundException -> HttpStatus.NOT_FOUND
is PromotionRequestResolvedException -> HttpStatus.CONFLICT
is DeadlinePassedException -> HttpStatus.BAD_REQUEST
else -> TODO()
}

val HttpStatus.reason: String
get() = this.reasonPhrase

fun DomainException.toResponseEntity() =
ResponseEntity
.status(this.httpCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fun Homework.toMessage(): HomeworkMessage =
maxScore = this.maxScore.value.toInt(),
publicationMoment = this.publicationMoment,
creationMoment = this.creationMoment,
deadlineMoment = this.deadlineMoment,
)

fun Homework.Draft.toMessage(): HomeworkDraftMessage =
Expand All @@ -20,6 +21,7 @@ fun Homework.Draft.toMessage(): HomeworkDraftMessage =
description = this.description.text,
maxScore = this.maxScore.value.toInt(),
publicationMoment = this.publicationMoment,
deadlineMoment = this.deadlineMoment,
)

fun HomeworkDraftMessage.toModel(): Homework.Draft =
Expand All @@ -28,4 +30,5 @@ fun HomeworkDraftMessage.toModel(): Homework.Draft =
description = Homework.Description(this.description),
maxScore = Homework.Score(this.maxScore.toShort()),
publicationMoment = this.publicationMoment,
deadlineMoment = this.deadlineMoment,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ru.vityaman.lms.botalka.domain.exception

class DeadlinePassedException(message: String) :
DomainException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data class Homework(
val description: Description,
val maxScore: Score,
val publicationMoment: OffsetDateTime,
val deadlineMoment: OffsetDateTime,
val creationMoment: OffsetDateTime,
) {
@JvmInline
Expand Down Expand Up @@ -66,5 +67,12 @@ data class Homework(
val description: Description,
val maxScore: Score,
val publicationMoment: OffsetDateTime,
)
val deadlineMoment: OffsetDateTime,
) {
init {
expect(publicationMoment.plusMinutes(1).isBefore(deadlineMoment)) {
append("Homework must be published 1 minute before deadline")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import ru.vityaman.lms.botalka.domain.model.Homework

interface HomeworkService {
suspend fun create(homework: Homework.Draft): Homework
suspend fun getById(id: Homework.Id): Homework?
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ class BasicHomeworkService(
) : HomeworkService {
override suspend fun create(homework: Homework.Draft): Homework =
storage.create(homework)

override suspend fun getById(id: Homework.Id): Homework? =
storage.getById(id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import kotlinx.coroutines.flow.lastOrNull
import kotlinx.coroutines.flow.map
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import ru.vityaman.lms.botalka.domain.exception.DeadlinePassedException
import ru.vityaman.lms.botalka.domain.exception.orNotFound
import ru.vityaman.lms.botalka.domain.model.Homework
import ru.vityaman.lms.botalka.domain.model.Workspace
import ru.vityaman.lms.botalka.logic.HomeworkService
import ru.vityaman.lms.botalka.logic.WorkspaceService
import ru.vityaman.lms.botalka.storage.WorkspaceStorage
import java.time.Clock
import java.time.OffsetDateTime

@Service
class BasicWorkspaceService(
@Autowired private val storage: WorkspaceStorage,
@Autowired private val homeworks: HomeworkService,
@Autowired private val clock: Clock,
) : WorkspaceService {
override fun events(id: Workspace.Id): Flow<Workspace.Event> =
storage.events(id)
Expand All @@ -24,7 +31,7 @@ class BasicWorkspaceService(
): Workspace.Event =
when (event) {
is Workspace.Comment.Draft -> storage.save(id, event)
is Workspace.Submission.Draft -> storage.save(id, event)
is Workspace.Submission.Draft -> submit(id, event)
is Workspace.Feedback.Draft -> storage.save(id, event)
}

Expand All @@ -38,4 +45,22 @@ class BasicWorkspaceService(

override fun all(): Flow<Workspace.Id> =
storage.all()

private suspend fun submit(
id: Workspace.Id,
event: Workspace.Submission.Draft,
): Workspace.Submission {
val homework = homeworks.getById(id.homework)
.orNotFound("Homework with id ${id.homework.number} not found")
if (homework.deadlineMoment.isBefore(OffsetDateTime.now(clock))) {
throw DeadlinePassedException(
buildString {
append("Deadline for homework with ")
append("id ${homework.id.number} ")
append("\"${homework.title}\" was passed")
},
)
}
return storage.save(id, event)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import ru.vityaman.lms.botalka.domain.model.Homework

interface HomeworkStorage {
suspend fun create(homework: Homework.Draft): Homework
suspend fun getById(id: Homework.Id): Homework?
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ru.vityaman.lms.botalka.storage.jooq

import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import ru.vityaman.lms.botalka.domain.model.Homework
Expand All @@ -20,16 +21,25 @@ class JooqHomeworkStorage(
HOMEWORK.DESCRIPTION,
HOMEWORK.MAX_SCORE,
HOMEWORK.PUBLICATION_MOMENT,
HOMEWORK.DEADLINE_MOMENT,
)
.values(
homework.title.text,
homework.description.text,
homework.maxScore.value,
homework.publicationMoment,
homework.deadlineMoment,
)
.returningResult(HOMEWORK.fields().asList())
.coerce(HOMEWORK)
.toMono()
.map { it.toModel() }
.awaitSingle()

override suspend fun getById(id: Homework.Id): Homework? =
database.withDSLMono {
selectFrom(HOMEWORK)
.where(HOMEWORK.ID.eq(id.number))
.toMono()
}.awaitSingleOrNull()?.toModel()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ fun HomeworkRecord.toModel(): Homework =
maxScore = Homework.Score(this.maxScore),
publicationMoment = this.publicationMoment,
creationMoment = this.creationMoment!!,
deadlineMoment = this.deadlineMoment,
)
1 change: 1 addition & 0 deletions botalka/src/main/resources/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ CREATE TABLE lms.homework (
description text NOT NULL,
max_score lms.score NOT NULL,
publication_moment timestamptz NOT NULL,
deadline_moment timestamptz NOT NULL,
creation_moment timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Expand Down
10 changes: 9 additions & 1 deletion botalka/src/main/resources/static/openapi/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ paths:
schema:
$ref: '#/components/schemas/WorkspaceEvent'
400:
description: Event is invalid
description: Event is invalid or deadline has passed
content:
application/json:
schema:
Expand Down Expand Up @@ -296,6 +296,11 @@ components:
format: date-time
description: A moment when this homework must be published
example: 2024-03-11T12:00:00Z
HomeworkDeadlineMoment:
type: string
format: date-time
description: A moment when is a deadline for this homework
example: 2024-03-11T12:00:00Z
CreationMoment:
type: string
format: date-time
Expand All @@ -312,11 +317,14 @@ components:
$ref: '#/components/schemas/HomeworkScore'
publication_moment:
$ref: '#/components/schemas/HomeworkPublicationMoment'
deadline_moment:
$ref: '#/components/schemas/HomeworkDeadlineMoment'
required:
- title
- description
- max_score
- publication_moment
- deadline_moment
Homework:
allOf:
- $ref: '#/components/schemas/HomeworkDraft'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package ru.vityaman.lms.botalka.api.http.endpoint

import io.kotest.matchers.collections.shouldBeUnique
import io.kotest.matchers.date.shouldBeAfter
import io.kotest.matchers.date.shouldBeBefore
import io.kotest.matchers.date.shouldHaveSameInstantAs
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.async
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.reactive.function.client.WebClientResponseException
import ru.vityaman.lms.botalka.BotalkaTestSuite
import ru.vityaman.lms.botalka.api.http.client.HomeworkDraftMessage
import ru.vityaman.lms.botalka.api.http.client.apis.HomeworkApi
Expand All @@ -23,24 +26,28 @@ class HomeworkApiTest(
description = "Test Homework 1 Description",
maxScore = 100,
publicationMoment = time("2024-04-01T10:15:30+03:00"),
deadlineMoment = time("2024-05-02T10:11:33+03:00"),
),
HomeworkDraftMessage(
title = "Test Homework 2",
description = "Test Homework 2 Description",
maxScore = 250,
publicationMoment = time("2024-05-01T12:00:30+03:00"),
deadlineMoment = time("2024-07-01T12:11:30+03:00"),
),
HomeworkDraftMessage(
title = "Test Homework 3",
description = "Test Homework 3 Description",
maxScore = 100,
publicationMoment = time("2024-05-02T12:00:30+03:00"),
deadlineMoment = time("2024-05-03T06:11:30+03:00"),
),
HomeworkDraftMessage(
title = "Test Homework 4",
description = "Test Homework 4 Description",
maxScore = 300,
publicationMoment = time("2024-05-02T12:00:30+03:00"),
deadlineMoment = time("2024-05-05T06:10:30+03:00"),
),
)

Expand All @@ -59,17 +66,74 @@ class HomeworkApiTest(
l.description shouldBe r.description
l.maxScore shouldBe r.maxScore
l.publicationMoment shouldHaveSameInstantAs r.publicationMoment
l.deadlineMoment shouldHaveSameInstantAs r.deadlineMoment
}

val endingPoint = OffsetDateTime.now()

val results = draftToResultList.map { it.second }
results.forEach {
it.creationMoment shouldBeAfter startingPoint
it.creationMoment shouldBeBefore endingPoint
}

val ids = results.map { it.id }
ids.shouldBeUnique()
}

@Test
fun homeworkInvariants(): Unit = runBlocking {
assertThrows<WebClientResponseException.BadRequest> {
verifyTime(
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-04T05:05:05+03:00"),
)
}
assertThrows<WebClientResponseException.BadRequest> {
verifyTime(
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-05T05:05:45+03:00"),
)
}
assertThrows<WebClientResponseException.BadRequest> {
verifyTime(
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-05T05:06:05+03:00"),
)
}
verifyTime(
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-05T05:06:06+03:00"),
)
}

private suspend fun verifyTime(
publicationMoment: OffsetDateTime,
deadlineMoment: OffsetDateTime,
) {
api.homeworkPost(
sampleHomework(
publicationMoment = publicationMoment,
deadlineMoment = deadlineMoment,
),
).awaitSingle()
}

private fun time(format: String): OffsetDateTime =
OffsetDateTime.parse(format)

@Suppress("LongParameterList")
private fun sampleHomework(
title: String = "Sample Test Homework",
description: String = "Sample Test Homework Description",
maxScore: Int = 100,
publicationMoment: OffsetDateTime = time("2024-05-02T12:00:30+03:00"),
deadlineMoment: OffsetDateTime = time("2024-05-03T06:11:30+03:00"),
) = HomeworkDraftMessage(
title = title,
description = description,
maxScore = maxScore,
publicationMoment = publicationMoment,
deadlineMoment = deadlineMoment,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class RatingApiTest(
description = "Description Any",
maxScore = 200,
publicationMoment = OffsetDateTime.now(),
deadlineMoment = OffsetDateTime.now().plusDays(1),
)

private val dummySubmit =
Expand Down
Loading

0 comments on commit 8fbfff7

Please sign in to comment.