Skip to content

Commit

Permalink
Merge pull request #40 from jvmusin/make-name-changeable
Browse files Browse the repository at this point in the history
Make name changeable
  • Loading branch information
jvmusin authored Mar 18, 2024
2 parents ebb7899 + 2c9459d commit 4dc12b8
Show file tree
Hide file tree
Showing 16 changed files with 130 additions and 157 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ out/
.kotlin

### Converter ###
polygon-problems
sybon-packages
ready
src/main/resources/application.yaml
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dependencies {

implementation("org.jsoup:jsoup:$jsoupVersion")

implementation("org.apache.commons:commons-compress:1.26.1") // For reading zip archives from Polygon

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
testImplementation("io.kotest.extensions:kotest-extensions-spring:$kotestExtensionsSpringVersion")
Expand Down
25 changes: 11 additions & 14 deletions frontend/src/lib/polybacs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,21 @@ export function useNameAvailability(
return result
}

type AdditionalProperties = {
name: string
prefix: string
suffix: string
timeLimitMillis: number
memoryLimitMegabytes: number
statementFormat: string
}

export function downloadProblem({
problemId,
additionalProperties,
}: {
problemId: number
additionalProperties: {
prefix: string
suffix: string
timeLimitMillis: number
memoryLimitMegabytes: number
statementFormat: string
}
additionalProperties: AdditionalProperties
}) {
fetch(`${baseUrl}/problems/${problemId}/download`, {
method: 'POST',
Expand Down Expand Up @@ -123,13 +126,7 @@ export function transferProblem({
additionalProperties,
}: {
problemId: number
additionalProperties: {
prefix: string
suffix: string
timeLimitMillis: number
memoryLimitMegabytes: number
statementFormat: string
}
additionalProperties: AdditionalProperties
}) {
fetch(`${baseUrl}/problems/${problemId}/transfer`, {
method: 'POST',
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/routes/problem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ProblemInfo,
downloadProblem,
getProblemInfo,
transferProblem as transferProblem,
transferProblem,
useNameAvailability,
} from '@/lib/polybacs-api'
import { zodResolver } from '@hookform/resolvers/zod'
Expand Down Expand Up @@ -114,7 +114,7 @@ function NameModifiersCard({ form }: { form: FormType }) {
</Field>
<Field>
<Label>Name</Label>
<Input {...form.register('name')} type="text" disabled />
<Input {...form.register('name')} type="text" />
</Field>
<Field>
<Label>Suffix</Label>
Expand Down Expand Up @@ -218,6 +218,7 @@ function Footer({ info, form }: { info: ProblemInfo; form: FormType }) {
return {
problemId: info.problem.id,
additionalProperties: {
name: data.name,
prefix: data.prefix,
suffix: data.suffix,
timeLimitMillis: data.timeLimitMillis,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,13 @@ import io.github.jvmusin.polybacs.api.StatementFormat.PDF
* @param statementFormat format of the statement, actually `PDF` or `HTML`.
*/
data class AdditionalProblemProperties(
val prefix: String? = null,
val name: String,
val prefix: String? = null, // TODO: Drop nullability; drop whole concept?
val suffix: String? = null,
val timeLimitMillis: Int? = null,
val memoryLimitMegabytes: Int? = null,
val statementFormat: StatementFormat = PDF
) {
companion object {
/** Do not add any prefix/suffix and use problem's default time and memory limits. */
val defaultProperties = AdditionalProblemProperties()
}

/** Build problem name prefixing it with [prefix] and suffixing with [suffix] if they are not `null`. */
fun buildFullName(problemName: String) = "${prefix.orEmpty()}$problemName${suffix.orEmpty()}"
fun buildFullName() = "${prefix.orEmpty()}$name${suffix.orEmpty()}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ class BacsArchiveService(
/** Uploads [problem] to Bacs archive with extra [properties]. */
suspend fun uploadProblem(
problem: IRProblem,
properties: AdditionalProblemProperties = AdditionalProblemProperties.defaultProperties,
properties: AdditionalProblemProperties = AdditionalProblemProperties(problem.name),
): String {
val zip = problem.toZipArchive(properties)
uploadProblem(zip)
val fullName = properties.buildFullName(problem.name)
val fullName = properties.buildFullName()
val state = waitTillProblemIsImported(fullName)
if (state != BacsProblemState.IMPORTED)
throw BacsProblemUploadException("Задача $fullName не импортирована, статус $state")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package io.github.jvmusin.polybacs.polygon

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration

/**
* Polygon API configuration properties.
*
* @property apiKey apiKey to access the API.
* @property secret secret key to access the API.
*/
@Configuration
data class PolygonConfig(
val apiKey: String,
val secret: String
@Value("\${polygon.apiKey}") val apiKey: String,
@Value("\${polygon.secret}") val secret: String,
)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.notExists
import kotlin.io.path.readText

/**
* Polygon problem downloader
*
* Used for downloading problems from Polygon.
*/
interface PolygonProblemDownloader {
/**
* Downloads the problem with the given [problemId].
*
* Tests might be skipped by setting [includeTests].
*
* @param problemId id of the problem to download.
* @param includeTests if true then the problem tests will also be downloaded.
* @return The problem with or without tests, depending on [includeTests] parameter.
* @throws NoSuchProblemException if the problem does not exist.
* @throws AccessDeniedException if not enough rights to download the problem.
* @throws ProblemDownloadingException if something gone wrong while downloading the problem.
*/
suspend fun downloadProblem(
problemId: Int,
includeTests: Boolean,
statementFormat: StatementFormat = StatementFormat.PDF,
): IRProblem
}

class PolygonProblemDownloaderImpl(
@Component
class PolygonProblemDownloader(
private val polygonApi: PolygonApi,
) : PolygonProblemDownloader {
) {

/**
* Full package id.
Expand Down Expand Up @@ -182,13 +162,11 @@ class PolygonProblemDownloaderImpl(
*/
private suspend fun downloadChecker(problemId: Int, packageId: Int): IRChecker {
val name = "check.cpp"
val file = polygonApi.downloadPackage(problemId, packageId).resolve(name)
if (file.notExists()) {
throw CheckerNotFoundException(
val file = polygonApi.getFileFromZipPackage(problemId, packageId, name)
?: throw CheckerNotFoundException(
"Не найден чекер '$name'. Другие чекеры не поддерживаются"
)
}
return IRChecker(name, file.readText())
return IRChecker(name, file.decodeToString())
}

/**
Expand Down Expand Up @@ -391,45 +369,60 @@ class PolygonProblemDownloaderImpl(
cache[FullPackageId(packageId, includeTests, statementFormat)] = problem
}

override suspend fun downloadProblem(problemId: Int, includeTests: Boolean, statementFormat: StatementFormat) =
withContext(Dispatchers.IO) {
// eagerly check for access
val problem = getProblem(problemId)

val packageId = polygonApi.getLatestPackageId(problemId)

val cached = getProblemFromCache(packageId, includeTests, statementFormat)
if (cached != null) return@withContext cached

val info = async { getProblemInfo(problemId) }
val statement = async { downloadStatement(problemId, packageId, statementFormat) }
val checker = async { downloadChecker(problemId, packageId) }

val testsAndTestGroups = async {
/*
* These methods can throw an exception about incorrectly formatted problem,
* so throw them as soon as possible before downloading tests data.
*/
run {
info.await()
statement.await()
checker.await()
}
getTestsAndTestGroups(problemId, includeTests)
/**
* Downloads the problem with the given [problemId].
*
* Tests might be skipped by setting [includeTests].
*
* @param problemId id of the problem to download.
* @param includeTests if true then the problem tests will also be downloaded.
* @return The problem with or without tests, depending on [includeTests] parameter.
* @throws NoSuchProblemException if the problem does not exist.
* @throws AccessDeniedException if not enough rights to download the problem.
* @throws ProblemDownloadingException if something gone wrong while downloading the problem.
*/
suspend fun downloadProblem(
problemId: Int,
includeTests: Boolean,
statementFormat: StatementFormat = StatementFormat.PDF
): IRProblem = withContext(Dispatchers.IO) {
// eagerly check for access
val problem = getProblem(problemId)

val packageId = polygonApi.getLatestPackageId(problemId)

val cached = getProblemFromCache(packageId, includeTests, statementFormat)
if (cached != null) return@withContext cached

val info = async { getProblemInfo(problemId) }
val statement = async { downloadStatement(problemId, packageId, statementFormat) }
val checker = async { downloadChecker(problemId, packageId) }

val testsAndTestGroups = async {
/*
* These methods can throw an exception about incorrectly formatted problem,
* so throw them as soon as possible before downloading tests data.
*/
run {
info.await()
statement.await()
checker.await()
}

val solutions = async { getSolutions(problemId) }
val limits = async { with(info.await()) { IRLimits(timeLimit, memoryLimit) } }

IRProblem(
name = problem.name,
owner = problem.owner,
statement = statement.await(),
limits = limits.await(),
tests = testsAndTestGroups.await().first,
groups = testsAndTestGroups.await().second,
checker = checker.await(),
solutions = solutions.await()
).also { saveProblemToCache(packageId, includeTests, statementFormat, it) }
getTestsAndTestGroups(problemId, includeTests)
}

val solutions = async { getSolutions(problemId) }
val limits = async { with(info.await()) { IRLimits(timeLimit, memoryLimit) } }

IRProblem(
name = problem.name,
owner = problem.owner,
statement = statement.await(),
limits = limits.await(),
tests = testsAndTestGroups.await().first,
groups = testsAndTestGroups.await().second,
checker = checker.await(),
solutions = solutions.await()
).also { saveProblemToCache(packageId, includeTests, statementFormat, it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import io.github.jvmusin.polybacs.polygon.api.PolygonApi
import io.github.jvmusin.polybacs.polygon.exception.downloading.ProblemDownloadingException
import io.github.jvmusin.polybacs.polygon.exception.response.AccessDeniedException
import io.github.jvmusin.polybacs.polygon.exception.response.NoSuchProblemException
import org.springframework.stereotype.Service

/**
* Polygon service.
*
* Used to communicate to Polygon API.
*/
@Service
class PolygonService(
private val polygonApi: PolygonApi,
private val problemDownloader: PolygonProblemDownloader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ interface PolygonApi {
suspend fun getPackage(
@RequestParam("problemId") problemId: Int,
@RequestParam("packageId") packageId: Int,
): ByteArray // TODO: Check if it works
): ByteArray

@PostExchange("contest.problems")
suspend fun getContestProblems(
Expand Down
Loading

0 comments on commit 4dc12b8

Please sign in to comment.