Skip to content

Commit

Permalink
feat: advanced solution filters (#41)
Browse files Browse the repository at this point in the history
* feat(server): support advanced solution filter

* feat: supported advanced solution filter
  • Loading branch information
thezzisu authored Jan 24, 2024
1 parent aec22c9 commit ab6d379
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 57 deletions.
3 changes: 3 additions & 0 deletions .yarn/versions/901ad692.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
releases:
"@aoi-js/frontend": patch
"@aoi-js/server": patch
25 changes: 21 additions & 4 deletions apps/frontend/src/components/solution/SolutionList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@update:options="({ page, itemsPerPage }) => submissions.execute(0, page, itemsPerPage)"
>
<template v-slot:[`item.state`]="{ item }">
<SolutionStateChip :state="item.state" />
<SolutionStateChip :state="item.state" @click.right="state = '' + item.state" />
</template>
<template v-slot:[`item.userId`]="{ item }">
<PrincipalProfile :principal-id="item.userId" />
Expand All @@ -28,7 +28,12 @@
</RouterLink>
</template>
<template v-slot:[`item.status`]="{ item }">
<SolutionStatusChip v-if="item.status" :status="item.status" :to="rel(item._id)" />
<SolutionStatusChip
v-if="item.status"
:status="item.status"
:to="rel(item._id)"
@click.right="status = item.status"
/>
<span v-else>-</span>
</template>
<template v-slot:[`item.score`]="{ item }">
Expand All @@ -47,7 +52,7 @@ import PrincipalProfile from '../utils/PrincipalProfile.vue'
import SolutionScoreDisplay from './SolutionScoreDisplay.vue'
import SolutionStatusChip from './SolutionStatusChip.vue'
import { usePagination } from '@/utils/pagination'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useAppState } from '@/stores/app'
import { useContestProblemTitle } from '@/utils/contest/problem/inject'
import type { ISolutionDTO } from './types'
Expand Down Expand Up @@ -82,6 +87,12 @@ const headersProblem = [
const userId = useRouteQuery('userId')
const contestProblemId = useRouteQuery('problemId')
const state = useRouteQuery('state')
const status = useRouteQuery('status')
const submittedL = useRouteQuery('submittedL')
const submittedR = useRouteQuery('submittedR')
const scoreL = useRouteQuery('scoreL')
const scoreR = useRouteQuery('scoreR')
const {
page,
Expand All @@ -93,7 +104,13 @@ const {
Object.fromEntries(
Object.entries({
userId,
problemId: contestProblemId
problemId: contestProblemId,
state,
status,
submittedL,
submittedR,
scoreL,
scoreR
})
.map(([k, v]) => [k, v.value])
.filter(([, v]) => v)
Expand Down
16 changes: 3 additions & 13 deletions apps/frontend/src/components/utils/DateTimeInput.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
<template>
<VTextField
:error="error"
v-model="value"
:label="label"
:disabled="disabled"
type="datetime-local"
/>
<VTextField v-bind="$attrs" :error="error" v-model="value" type="datetime-local" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { computed } from 'vue'
const model = defineModel<number>({ required: true })
defineProps<{
label?: string
disabled?: boolean
}>()
const model = defineModel<number>()
function toDateTimeLocalString(date: Date) {
const offset = date.getTimezoneOffset() * 60000
Expand All @@ -28,7 +18,7 @@ const error = ref(false)
// convert model to datetime-local format
const value = computed({
get: () => toDateTimeLocalString(new Date(model.value)),
get: () => toDateTimeLocalString(new Date(model.value ?? 0)),
set: (val) => {
const date = +new Date(val)
if (Number.isNaN(date)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,82 @@
</VCardSubtitle>
<VCardText>
<VForm fast-fail validate-on="submit lazy" @submit.prevent="rejudgeAllTask.execute">
<VTextField
v-model="rejudgeOptions.problemId"
:label="t('term.problem-id')"
:rules="[isUUID, isNotEmpty]"
:append-icon="rejudgeOptions.problemId === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.problemId = undefined"
/>
<VSelect
v-model="rejudgeOptions.state"
:items="[
{ title: 'Pending', value: 1 },
{ title: 'Queued', value: 2 },
{ title: 'Running', value: 3 },
{ title: 'Completed', value: 4 }
]"
:label="t('term.state')"
:append-icon="rejudgeOptions.state === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.state = undefined"
/>
<VTextField
v-model="rejudgeOptions.status"
:label="t('term.status')"
:append-icon="rejudgeOptions.status === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.status = undefined"
/>
<VTextField
v-model="rejudgeOptions.runnerId"
:rules="[isUUID]"
:label="t('term.runner-id')"
:append-icon="rejudgeOptions.runnerId === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.runnerId = undefined"
/>
<VRow>
<VCol cols="12">
<VTextField
v-model="rejudgeOptions.problemId"
:label="t('term.problem-id')"
:rules="[isUUID, isNotEmpty]"
:append-icon="rejudgeOptions.problemId === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.problemId = undefined"
/>
</VCol>
<VCol cols="6">
<VSelect
v-model="rejudgeOptions.state"
:items="[
{ title: 'Pending', value: 1 },
{ title: 'Queued', value: 2 },
{ title: 'Running', value: 3 },
{ title: 'Completed', value: 4 }
]"
:label="t('term.state')"
:append-icon="rejudgeOptions.state === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.state = undefined"
/>
</VCol>
<VCol cols="6">
<VTextField
v-model="rejudgeOptions.status"
:label="t('term.status')"
:append-icon="rejudgeOptions.status === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.status = undefined"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="rejudgeOptions.runnerId"
:rules="[isUUID]"
:label="t('term.runner-id')"
:append-icon="rejudgeOptions.runnerId === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.runnerId = undefined"
/>
</VCol>
<VCol cols="6">
<VTextField
v-model.number="rejudgeOptions.scoreL"
:rules="[(v) => v === undefined || v >= 0, (v) => v === undefined || v <= 100]"
:label="t('min-score')"
:append-icon="rejudgeOptions.scoreL === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.scoreL = undefined"
/>
</VCol>
<VCol cols="6">
<VTextField
v-model.number="rejudgeOptions.scoreR"
:rules="[(v) => v === undefined || v >= 0, (v) => v === undefined || v <= 100]"
:label="t('max-score')"
:append-icon="rejudgeOptions.scoreR === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.scoreR = undefined"
/>
</VCol>
<VCol cols="6">
<DateTimeInput
v-model="rejudgeOptions.submittedAtL"
:label="t('submitted-after')"
:append-icon="rejudgeOptions.submittedAtL === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.submittedAtL = undefined"
/>
</VCol>
<VCol cols="6">
<DateTimeInput
v-model="rejudgeOptions.submittedAtR"
:label="t('submitted-before')"
:append-icon="rejudgeOptions.submittedAtR === undefined ? 'mdi-null' : 'mdi-delete'"
@click:append="rejudgeOptions.submittedAtR = undefined"
/>
</VCol>
</VRow>
<VBtn
color="red"
variant="elevated"
Expand Down Expand Up @@ -109,6 +153,7 @@
<script setup lang="ts">
import type { IContestDTO } from '@/components/contest/types'
import AccessLevelEditor from '@/components/utils/AccessLevelEditor.vue'
import DateTimeInput from '@/components/utils/DateTimeInput.vue'
import { useAsyncTask, withMessage, noMessage } from '@/utils/async'
import { http } from '@/utils/http'
import { useAsyncState } from '@vueuse/core'
Expand Down Expand Up @@ -142,7 +187,11 @@ const rejudgeOptions = reactive({
problemId: undefined as string | undefined,
state: undefined as number | undefined,
status: undefined as string | undefined,
runnerId: undefined as string | undefined
runnerId: undefined as string | undefined,
scoreL: undefined as number | undefined,
scoreR: undefined as number | undefined,
submittedAtL: undefined as number | undefined,
submittedAtR: undefined as number | undefined
})
const rejudgeAllTask = useAsyncTask(async (ev: SubmitEventPromise) => {
const result = await ev
Expand Down Expand Up @@ -207,6 +256,10 @@ const isNotEmpty = (value: string | undefined) => {
en:
ranklist-state: Ranklist State is {state}
reset-runner: Switch Reset Runner
min-score: Min Score
max-score: Max Score
submitted-after: Submitted After
submitted-before: Submitted Before
action:
update-ranklists: Update ranklists
hint:
Expand All @@ -215,6 +268,10 @@ en:
zh-Hans:
ranklist-state: '排行榜状态: {state}'
reset-runner: 切换重置运行器
min-score: 最小分数
max-score: 最大分数
submitted-after: 提交时间晚于
submitted-before: 提交时间早于
action:
update-ranklists: 更新排行榜
hint:
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/routes/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,13 @@ export function loadCapability(
export function md5(email: string) {
return createHash('md5').update(email).digest('hex')
}

export function generateRangeQuery(L?: number, R?: number) {
return L === undefined
? R === undefined
? undefined
: { $lte: R }
: R === undefined
? { $gte: L }
: { $gte: L, $lte: R }
}
12 changes: 9 additions & 3 deletions apps/server/src/routes/contest/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../../index.js'
import { ensureCapability } from '../../utils/index.js'
import { manageACL, manageAccessLevel } from '../common/access.js'
import { defineRoutes } from '../common/index.js'
import { defineRoutes, generateRangeQuery } from '../common/index.js'
import { SContestStage } from '../../schemas/contest.js'
import { kContestContext } from './inject.js'
import { UUID } from 'mongodb'
Expand Down Expand Up @@ -93,7 +93,11 @@ export const contestAdminRoutes = defineRoutes(async (s) => {
problemId: Type.Optional(Type.UUID()),
state: Type.Optional(Type.Integer({ minimum: 1, maximum: 4 })),
status: Type.Optional(Type.String()),
runnerId: Type.Optional(Type.String())
runnerId: Type.Optional(Type.String()),
scoreL: Type.Optional(Type.Number()),
scoreR: Type.Optional(Type.Number()),
submittedAtL: Type.Optional(Type.Integer()),
submittedAtR: Type.Optional(Type.Integer())
}),
response: {
200: Type.Object({
Expand All @@ -114,7 +118,9 @@ export const contestAdminRoutes = defineRoutes(async (s) => {
? req.body.runnerId
? new UUID(req.body.runnerId)
: { $exists: false }
: undefined
: undefined,
score: generateRangeQuery(req.body.scoreL, req.body.scoreR),
submittedAt: generateRangeQuery(req.body.submittedAtL, req.body.submittedAtR)
},
[
{
Expand Down
19 changes: 17 additions & 2 deletions apps/server/src/routes/contest/solution/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Type } from '@sinclair/typebox'
import { defineRoutes, loadUUID, paramSchemaMerger } from '../../common/index.js'
import {
defineRoutes,
generateRangeQuery,
loadUUID,
paramSchemaMerger
} from '../../common/index.js'
import { findPaginated, hasCapability } from '../../../utils/index.js'
import { ContestCapability, ISolution, SolutionState, solutions } from '../../../db/index.js'
import { BSON } from 'mongodb'
Expand Down Expand Up @@ -226,6 +231,12 @@ export const contestSolutionRoutes = defineRoutes(async (s) => {
querystring: Type.Object({
userId: Type.Optional(Type.String()),
problemId: Type.Optional(Type.String()),
state: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })),
status: Type.Optional(Type.String()),
scoreL: Type.Optional(Type.Number()),
scoreR: Type.Optional(Type.Number()),
submittedAtL: Type.Optional(Type.Integer()),
submittedAtR: Type.Optional(Type.Integer()),
page: Type.Integer({ minimum: 1, default: 1 }),
perPage: Type.Integer({ enum: [15, 30] }),
count: Type.Boolean({ default: false })
Expand Down Expand Up @@ -263,7 +274,11 @@ export const contestSolutionRoutes = defineRoutes(async (s) => {
{
contestId: ctx._contest._id,
problemId: req.query.problemId ? new BSON.UUID(req.query.problemId) : undefined,
userId: req.query.userId ? new BSON.UUID(req.query.userId) : undefined
userId: req.query.userId ? new BSON.UUID(req.query.userId) : undefined,
state: req.query.state,
status: req.query.status,
score: generateRangeQuery(req.query.scoreL, req.query.scoreR),
submittedAt: generateRangeQuery(req.query.submittedAtL, req.query.submittedAtR)
},
{
projection: {
Expand Down
14 changes: 12 additions & 2 deletions apps/server/src/routes/problem/solution.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type } from '@sinclair/typebox'
import { defineRoutes, loadUUID, paramSchemaMerger } from '../common/index.js'
import { defineRoutes, generateRangeQuery, loadUUID, paramSchemaMerger } from '../common/index.js'
import { findPaginated, hasCapability } from '../../utils/index.js'
import { ISolution, ProblemCapability, SolutionState, solutions } from '../../db/index.js'
import { BSON } from 'mongodb'
Expand Down Expand Up @@ -178,6 +178,12 @@ export const problemSolutionRoutes = defineRoutes(async (s) => {
description: 'Get problem solutions',
querystring: Type.Object({
userId: Type.Optional(Type.String()),
state: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })),
status: Type.Optional(Type.String()),
scoreL: Type.Optional(Type.Number()),
scoreR: Type.Optional(Type.Number()),
submittedAtL: Type.Optional(Type.Integer()),
submittedAtR: Type.Optional(Type.Integer()),
page: Type.Integer({ minimum: 1, default: 1 }),
perPage: Type.Integer({ enum: [15, 30] }),
count: Type.Boolean({ default: false })
Expand Down Expand Up @@ -217,7 +223,11 @@ export const problemSolutionRoutes = defineRoutes(async (s) => {
{
problemId: ctx._problemId,
contestId: { $exists: false },
userId: req.query.userId ? new BSON.UUID(req.query.userId) : undefined
userId: req.query.userId ? new BSON.UUID(req.query.userId) : undefined,
state: req.query.state,
status: req.query.status,
score: generateRangeQuery(req.query.scoreL, req.query.scoreR),
submittedAt: generateRangeQuery(req.query.submittedAtL, req.query.submittedAtR)
},
{
projection: {
Expand Down

0 comments on commit ab6d379

Please sign in to comment.